Skip to content

Commit f7e5c80

Browse files
authored
Merge pull request #300 from patchlevel/improve-hydrator-integration
add DatePoint & Uid normalizer and hydrator guesser injection
2 parents 42d6a59 + 852f6c2 commit f7e5c80

35 files changed

+768
-367
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"require": {
2121
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
2222
"patchlevel/event-sourcing": "^3.18.0",
23+
"patchlevel/hydrator": "^1.18.0",
2324
"symfony/cache": "^6.4.0 || ^7.0.0 || ^8.0.0",
2425
"symfony/config": "^6.4.0 || ^7.0.0 || ^8.0.0",
2526
"symfony/console": "^6.4.1 || ^7.0.1 || ^8.0.0",
@@ -40,6 +41,7 @@
4041
"phpstan/phpstan-symfony": "^2.0.8",
4142
"phpunit/phpunit": "^11.5.45",
4243
"roave/security-advisories": "dev-master",
44+
"symfony/clock": "^6.4.0 || ^7.0.0 || ^8.0.0",
4345
"symfony/uid": "^6.4.0 || ^7.0.0 || ^8.0.0",
4446
"symfony/var-dumper": "^6.4.0 || ^7.0.0 || ^8.0.0",
4547
"symfony/web-profiler-bundle": "^6.4.0 || ^7.0.0 || ^8.0.0",

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/pages/configuration.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,10 @@ patchlevel_event_sourcing:
496496
default_exceptions:
497497
- Patchlevel\EventSourcing\Repository\AggregateOutdated
498498
```
499-
500499
!!! note
501500

502501
You can find out more about instant retry [here](https://event-sourcing.patchlevel.io/latest/command_bus/#instant-retry).
503-
502+
504503
## Query Bus
505504

506505
You can enable the query bus integration to use queries to retrieve data from your system. For this bundle we provide
@@ -675,13 +674,12 @@ patchlevel_event_sourcing:
675674
cryptography:
676675
use_encrypted_field_name: true
677676
```
678-
679677
!!! tip
680678

681679
You should activate `use_encrypted_field_name` to mark the fields that are encrypted.
682680
That allows you later to migrate not encrypted fields to encrypted fields.
683681
If you have already encrypted fields, you can activate `fallback_to_field_name` to use the old field name as fallback.
684-
682+
685683
If you want to use another algorithm, you can specify this here:
686684

687685
```yaml

docs/pages/usage.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,48 @@ This bundle adds more Symfony specific normalizers in addition to the existing b
153153

154154
You can find the other build-in normalizers [here](https://event-sourcing.patchlevel.io/latest/normalizer/#built-in-normalizer)
155155

156-
### Uuid
156+
!!! tip
157+
158+
The Hydrator can automatically determine the appropriate normalizer based on the data type and annotations.
159+
You don't have to specify the normalizer manually like in the example below.
160+
161+
### Uid
157162

158-
With the `Uuid` Normalizer, as the name suggests, you can convert Symfony Uuid objects to a string and back again.
163+
With the `Uid` Normalizer, as the name suggests, you can convert Symfony Uuid and Ulid objects to a string and back again.
159164

160165
```php
161-
use Patchlevel\EventSourcingBundle\Normalizer\SymfonyUuidNormalizer;
166+
use Patchlevel\EventSourcingBundle\Normalizer\UidNormalizer;
162167
use Symfony\Component\Uid\Uuid;
163168
164169
final class DTO
165170
{
166-
#[SymfonyUuidNormalizer]
171+
#[UidNormalizer]
167172
public Uuid $id;
168173
}
169174
```
170175
!!! warning
171176

172-
The symfony uuid don't implement the `AggregateId` interface, so it can be used as aggregate id.
177+
The symfony uuid don't implement the `AggregateId` interface, so it can not be used as an aggregate id directly.
178+
Use instead the `Patchlevel\EventSourcing\Aggregate\Uuid` class.
173179

174180
!!! tip
175181

176182
Use the `Uuid` implementation and `IdNormalizer` from the library to use it as an aggregate id.
177183

184+
### DatePoint
185+
186+
With the `DatePoint` Normalizer, you can convert a `DatePoint` object to a string and back again.
187+
188+
```php
189+
use Patchlevel\EventSourcingBundle\Normalizer\DatePointNormalizer;
190+
use Symfony\Component\Clock\DatePoint;
191+
192+
final class DTO
193+
{
194+
#[DatePointNormalizer]
195+
public DatePoint $createdAt;
196+
}
197+
```
178198
## Upcasting
179199

180200
```php

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<arg name="cache" value=".phpcs-cache"/>
77

88
<file>src</file>
9+
<file>tests</file>
910

1011
<rule ref="PatchlevelCodingStandard">
1112
<exclude name="Generic.Files.LineLength.TooLong" />

phpstan-baseline.neon

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ parameters:
1818
count: 1
1919
path: src/DependencyInjection/QueryHandlerCompilerPass.php
2020

21+
-
22+
message: '#^Method Patchlevel\\EventSourcingBundle\\Normalizer\\SymfonyGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
23+
identifier: missingType.generics
24+
count: 1
25+
path: src/Normalizer/SymfonyGuesser.php
26+
27+
-
28+
message: '#^Parameter \#1 \$uidClass of class Patchlevel\\EventSourcingBundle\\Normalizer\\UidNormalizer constructor expects class\-string\<Symfony\\Component\\Uid\\AbstractUid\>\|null, string given\.$#'
29+
identifier: argument.type
30+
count: 1
31+
path: src/Normalizer/SymfonyGuesser.php
32+
33+
-
34+
message: '#^Property Patchlevel\\EventSourcingBundle\\Normalizer\\UidNormalizer\:\:\$uidClass \(class\-string\<Symfony\\Component\\Uid\\AbstractUid\>\|null\) does not accept string\.$#'
35+
identifier: assign.propertyType
36+
count: 1
37+
path: src/Normalizer/UidNormalizer.php
38+
39+
-
40+
message: '#^Method Fixtures\\DummyGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
41+
identifier: missingType.generics
42+
count: 1
43+
path: tests/Fixtures/DummyGuesser.php
44+
2145
-
2246
message: '#^Property Patchlevel\\EventSourcingBundle\\Tests\\Fixtures\\Profile\:\:\$id is never read, only written\.$#'
2347
identifier: property.onlyWritten

src/DependencyInjection/PatchlevelEventSourcingExtension.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
use Patchlevel\EventSourcingBundle\DataCollector\MessageCollectorEventBus;
127127
use Patchlevel\EventSourcingBundle\Doctrine\DbalConnectionFactory;
128128
use Patchlevel\EventSourcingBundle\EventBus\SymfonyEventBus;
129+
use Patchlevel\EventSourcingBundle\Normalizer\SymfonyGuesser;
129130
use Patchlevel\EventSourcingBundle\QueryBus\SymfonyQueryBus;
130131
use Patchlevel\EventSourcingBundle\RequestListener\AutoSetupListener;
131132
use Patchlevel\EventSourcingBundle\RequestListener\SubscriptionRebuildAfterFileChangeListener;
@@ -140,6 +141,9 @@
140141
use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
141142
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
142143
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
144+
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
145+
use Patchlevel\Hydrator\Guesser\ChainGuesser;
146+
use Patchlevel\Hydrator\Guesser\Guesser;
143147
use Patchlevel\Hydrator\Hydrator;
144148
use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
145149
use Patchlevel\Hydrator\Metadata\MetadataFactory;
@@ -463,7 +467,10 @@ static function (ChildDefinition $definition): void {
463467
continue;
464468
}
465469

466-
throw new InvalidArgumentException(sprintf('Unknown retry strategy type "%s"', $strategyConfig['type']));
470+
throw new InvalidArgumentException(sprintf(
471+
'Unknown retry strategy type "%s"',
472+
$strategyConfig['type'],
473+
));
467474
}
468475
}
469476

@@ -617,7 +624,24 @@ static function (ChildDefinition $definition): void {
617624

618625
private function configureHydrator(ContainerBuilder $container): void
619626
{
620-
$container->register(AttributeMetadataFactory::class);
627+
$container->register(ChainGuesser::class)
628+
->setArguments([new TaggedIteratorArgument('event_sourcing.hydrator.guesser')]);
629+
630+
$container->register(BuiltInGuesser::class)
631+
->addTag('event_sourcing.hydrator.guesser', ['priority' => -100]);
632+
633+
$container->register(SymfonyGuesser::class)
634+
->addTag('event_sourcing.hydrator.guesser', ['priority' => -50]);
635+
636+
$container->registerForAutoconfiguration(Guesser::class)
637+
->addTag('event_sourcing.hydrator.guesser');
638+
639+
$container->register(AttributeMetadataFactory::class)
640+
->setArguments([
641+
null,
642+
new Reference(ChainGuesser::class),
643+
]);
644+
621645
$container->setAlias(MetadataFactory::class, AttributeMetadataFactory::class);
622646

623647
$container->register(MetadataHydrator::class)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcingBundle\Normalizer;
6+
7+
use Attribute;
8+
use Patchlevel\Hydrator\Normalizer\InvalidArgument;
9+
use Patchlevel\Hydrator\Normalizer\Normalizer;
10+
use Symfony\Component\Clock\DatePoint;
11+
12+
use function is_string;
13+
14+
#[Attribute(Attribute::TARGET_PROPERTY)]
15+
final class DatePointNormalizer implements Normalizer
16+
{
17+
public function __construct(
18+
private string $format = DatePoint::ATOM,
19+
) {
20+
}
21+
22+
public function normalize(mixed $value): string|null
23+
{
24+
if ($value === null) {
25+
return null;
26+
}
27+
28+
if (!$value instanceof DatePoint) {
29+
throw InvalidArgument::withWrongType(DatePoint::class . '|null', $value);
30+
}
31+
32+
return $value->format($this->format);
33+
}
34+
35+
public function denormalize(mixed $value): DatePoint|null
36+
{
37+
if ($value === null) {
38+
return null;
39+
}
40+
41+
if (!is_string($value)) {
42+
throw InvalidArgument::withWrongType('string|null', $value);
43+
}
44+
45+
return DatePoint::createFromFormat($this->format, $value);
46+
}
47+
}

src/Normalizer/SymfonyGuesser.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcingBundle\Normalizer;
6+
7+
use Patchlevel\Hydrator\Guesser\Guesser;
8+
use Patchlevel\Hydrator\Normalizer\Normalizer;
9+
use Symfony\Component\Clock\DatePoint;
10+
use Symfony\Component\TypeInfo\Type\ObjectType;
11+
use Symfony\Component\Uid\AbstractUid;
12+
13+
final class SymfonyGuesser implements Guesser
14+
{
15+
public function guess(ObjectType $type): Normalizer|null
16+
{
17+
if ($type->isIdentifiedBy(AbstractUid::class)) {
18+
return new UidNormalizer($type->getClassName());
19+
}
20+
21+
if ($type->isIdentifiedBy(DatePoint::class)) {
22+
return new DatePointNormalizer();
23+
}
24+
25+
return null;
26+
}
27+
}

src/Normalizer/SymfonyUuidNormalizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use function is_string;
1313

14+
/** @deprecated Use SymfonyUidNormalizer instead, which supports all Uid types, not just Uuid. */
1415
#[Attribute(Attribute::TARGET_PROPERTY)]
1516
final class SymfonyUuidNormalizer implements Normalizer
1617
{

0 commit comments

Comments
 (0)