Skip to content

Commit

Permalink
allow to extend infer normalizerer with custom guesser
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Jul 8, 2024
1 parent ab7500a commit c1aeeea
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 25 deletions.
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ final class DTO
}
```

> [!TIP]
> You do not need to specify the DateTimeImmutableNormalizer, as the Hydrator can guess it from the type definition.
You can also define the format. Either describe it yourself as a string or use one of the existing constants.
The default is `DateTimeImmutable::ATOM`.

Expand Down Expand Up @@ -141,6 +144,9 @@ final class DTO
}
```

> [!TIP]
> You do not need to specify the DateTimeNormalizer, as the Hydrator can guess it from the type definition.
You can also specify the format here. The default is `DateTime::ATOM`.

```php
Expand Down Expand Up @@ -170,6 +176,9 @@ final class DTO
}
```

> [!TIP]
> You do not need to specify the DateTimeZoneNormalizer, as the Hydrator can guess it from the type definition.
#### Enum

Backed enums can also be normalized.
Expand All @@ -185,6 +194,9 @@ final class DTO
}
```

> [!TIP]
> You do not need to specify the EnumNormalizer, as the Hydrator can guess it from the type definition.
#### Object

If you have a complex object that you want to normalize, you can use the `ObjectNormalizer`.
Expand Down Expand Up @@ -325,11 +337,10 @@ final class DTO
}
```

### Infer Normalizer
### Guess Normalizer

We also integrated a process where the normalizer gets inferred by type. This means you don't need to define the
normalizer in for the properties or on class level. Right now this is only possible for Normalizer defined by our
library. There are exceptions though, the `ObjectNormalizer` and the `ArrayNormalizer`.
normalizer in for the properties or on class level.

These Normalizer can be inferred:

Expand All @@ -338,6 +349,43 @@ These Normalizer can be inferred:
* `DateTimeZoneNormalizer`
* `EnumNormalizer`

You can also create your own guesser:

```php
use Patchlevel\Hydrator\Guesser\Guesser;
use Patchlevel\Hydrator\Normalizer\Normalizer;

final class MyGuesser implements Guesser
{
/** @param class-string $className */
public function guess(string $className): Normalizer|null
{
if ($className === Name::class) {
return new NameNormalizer();
}

return null;
}
}
```

After that you can use the guesser in the hydrator.

```php
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;

$hydrator = new MetadataHydrator(
new AttributeMetadataFactory([
new MyGuesser(),
new BuiltInGuesser()
])
);
```

> [!WARNING]
> Do not forget to add the built-in guesser.
### Normalized Name

Expand Down Expand Up @@ -422,7 +470,7 @@ final class DTO
}
```

> [!DANGER]
> [!CAUTION]
> You have to deal with this case in your business logic such as aggregates and subscriptions.
> [!WARNING]
Expand Down
41 changes: 41 additions & 0 deletions src/Guesser/BuiltInGuesser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Guesser;

use BackedEnum;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
use Patchlevel\Hydrator\Normalizer\Normalizer;

use function is_a;

final class BuiltInGuesser implements Guesser
{
/** @param class-string $className */
public function guess(string $className): Normalizer|null
{
$normalizer = match ($className) {
DateTimeImmutable::class => new DateTimeImmutableNormalizer(),
DateTime::class => new DateTimeNormalizer(),
DateTimeZone::class => new DateTimeZoneNormalizer(),
default => null,
};

if ($normalizer) {
return $normalizer;
}

if (is_a($className, BackedEnum::class, true)) {
return new EnumNormalizer($className);
}

return null;
}
}
13 changes: 13 additions & 0 deletions src/Guesser/Guesser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Guesser;

use Patchlevel\Hydrator\Normalizer\Normalizer;

interface Guesser
{
/** @param class-string $className */
public function guess(string $className): Normalizer|null;
}
38 changes: 18 additions & 20 deletions src/Metadata/AttributeMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,12 @@

namespace Patchlevel\Hydrator\Metadata;

use BackedEnum;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\Ignore;
use Patchlevel\Hydrator\Attribute\NormalizedName;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Guesser\Guesser;
use Patchlevel\Hydrator\Normalizer\Normalizer;
use Patchlevel\Hydrator\Normalizer\ReflectionTypeAwareNormalizer;
use ReflectionAttribute;
Expand All @@ -26,13 +20,20 @@
use function array_key_exists;
use function array_values;
use function class_exists;
use function is_a;

final class AttributeMetadataFactory implements MetadataFactory
{
/** @var array<class-string, ClassMetadata> */
private array $classMetadata = [];

/** @param iterable<Guesser> $guessers */
public function __construct(
private readonly iterable $guessers = [
new BuiltInGuesser(),
],
) {
}

/**
* @param class-string<T> $class
*
Expand Down Expand Up @@ -172,19 +173,16 @@ private function inferNormalizer(ReflectionNamedType $type): Normalizer|null
{
$className = $type->getName();

$normalizer = match ($className) {
DateTimeImmutable::class => new DateTimeImmutableNormalizer(),
DateTime::class => new DateTimeNormalizer(),
DateTimeZone::class => new DateTimeZoneNormalizer(),
default => null,
};

if ($normalizer) {
return $normalizer;
if (!class_exists($className)) {
return null;
}

if (is_a($className, BackedEnum::class, true)) {
return new EnumNormalizer($className);
foreach ($this->guessers as $guesser) {
$normalizer = $guesser->guess($className);

if ($normalizer !== null) {
return $normalizer;
}
}

return null;
Expand Down
62 changes: 62 additions & 0 deletions tests/Unit/Guesser/BuiltInGuesserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Guesser;

use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
use Patchlevel\Hydrator\Tests\Unit\Fixture\Email;
use Patchlevel\Hydrator\Tests\Unit\Fixture\Status;
use PHPUnit\Framework\TestCase;

final class BuiltInGuesserTest extends TestCase
{
public function testNoMatch(): void
{
$guesser = new BuiltInGuesser();
self::assertNull($guesser->guess(Email::class));
}

public function testEnum(): void
{
$guesser = new BuiltInGuesser();
self::assertInstanceOf(
EnumNormalizer::class,
$guesser->guess(Status::class),
);
}

public function testDateTimeImmutable(): void
{
$guesser = new BuiltInGuesser();
self::assertInstanceOf(
DateTimeImmutableNormalizer::class,
$guesser->guess(DateTimeImmutable::class),
);
}

public function testDateTime(): void
{
$guesser = new BuiltInGuesser();
self::assertInstanceOf(
DateTimeNormalizer::class,
$guesser->guess(DateTime::class),
);
}

public function testDateTimeZone(): void
{
$guesser = new BuiltInGuesser();
self::assertInstanceOf(
DateTimeZoneNormalizer::class,
$guesser->guess(DateTimeZone::class),
);
}
}
2 changes: 1 addition & 1 deletion tests/Unit/MetadataHydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class MetadataHydratorTest extends TestCase

public function setUp(): void
{
$this->hydrator = new MetadataHydrator(new AttributeMetadataFactory());
$this->hydrator = new MetadataHydrator();
}

public function testExtract(): void
Expand Down

0 comments on commit c1aeeea

Please sign in to comment.