55
66# Hydrator
77
8- With this library you can hydrate objects from array into objects and back again
9- with a focus on data processing from and into a database.
10- It has now been outsourced by the [ event-sourcing] ( https://github.com/patchlevel/event-sourcing ) library as a separate library.
8+ This library enables seamless hydration of objects to arrays—and back again.
9+ It’s optimized for both developer experience (DX) and performance.
10+
11+ The library is a core component of [ patchlevel/event-sourcing] ( ttps://github.com/patchlevel/event-sourcing ) ,
12+ where it powers the storage and retrieval of thousands of objects.
13+
14+ Hydration is handled through normalizers, especially for complex data types.
15+ The system can automatically determine the appropriate normalizer based on the data type and PHPStan/Psalm annotations.
16+
17+ In most cases, no manual configuration is needed.
18+ And if customization is required, it can be done easily using attributes.
1119
1220## Installation
1321
@@ -22,75 +30,124 @@ To use the hydrator you just have to create an instance of it.
2230``` php
2331use Patchlevel\Hydrator\MetadataHydrator;
2432
25- $hydrator = new MetadataHydrator();
33+ $hydrator = MetadataHydrator::create ();
2634```
2735
2836After that you can hydrate any classes or objects. Also ` final ` , ` readonly ` classes with ` property promotion ` .
37+ These objects or classes can have complex structures in the form of value objects, DTOs or collections.
38+ Or all nested together. Here's an example:
2939
3040``` php
3141final readonly class ProfileCreated
3242{
43+ /**
44+ * @param list<Skill > $skills
45+ */
3346 public function __construct(
34- public string $id,
35- public string $name
47+ public int $id,
48+ public string $name,
49+ public Role $role, // enum,
50+ public array $skills, // array of objects
51+ public DateTimeImmutable $createdAt,
3652 ) {
3753 }
3854}
3955```
4056
4157### Extract Data
4258
43- To convert objects into serializable arrays, you can use the ` extract ` method:
59+ To convert objects into serializable arrays, you can use the ` extract ` method of the hydrator.
4460
4561``` php
46- $event = new ProfileCreated('1', 'patchlevel');
62+ $event = new ProfileCreated(
63+ 1,
64+ 'patchlevel',
65+ Role::Admin,
66+ [new Skill('php', 10), new Skill('event-sourcing', 10)],
67+ new DateTimeImmutable('2023-10-01 12:00:00'),
68+ );
4769
4870$data = $hydrator->extract($event);
4971```
5072
73+ The result looks like this:
74+
5175``` php
5276[
53- 'id' => '1',
54- 'name' => 'patchlevel'
77+ 'id' => 1,
78+ 'name' => 'patchlevel',
79+ 'role' => 'admin',
80+ 'skills' => [
81+ [
82+ 'name' => 'php',
83+ 'level' => 10,
84+ ],
85+ [
86+ 'name' => 'event-sourcing',
87+ 'level' => 10,
88+ ],
89+ ],
90+ 'createdAt' => '2023-10-01T12:00:00+00:00',
5591]
5692```
5793
94+ We could now convert the whole thing into JSON using ` json_encode ` .
95+
5896### Hydrate Object
5997
98+ The process can also be reversed. Hydrate an array back into an object.
99+ To do this, we need to specify the class that should be created
100+ and the data that should then be written into it.
101+
60102``` php
61103$event = $hydrator->hydrate(
62104 ProfileCreated::class,
63105 [
64- 'id' => '1',
65- 'name' => 'patchlevel'
106+ 'id' => 1,
107+ 'name' => 'patchlevel',
108+ 'role' => 'admin',
109+ 'skills' => [
110+ [
111+ 'name' => 'php',
112+ 'level' => 10,
113+ ],
114+ [
115+ 'name' => 'event-sourcing',
116+ 'level' => 10,
117+ ],
118+ ],
119+ 'createdAt' => '2023-10-01T12:00:00+00:00',
66120 ]
67121);
68122
69123$oldEvent == $event // true
70124```
71125
126+ > [ !WARNING]
127+ > It is important to know that the constructor is not called!
128+
72129### Normalizer
73130
74- For more complex structures, i.e. non-scalar data types, we use normalizers.
75- We have some built-in normalizers for standard structures such as objects, enums, datetime etc.
131+ For more complex structures, i.e. non-scalar data types, we use normalizers.
132+ We have some built-in normalizers for standard structures such as objects, arrays, enums, datetime etc.
76133You can find the full list below.
77134
78- The normalizers can be set on each property by using the specific attribute.
79- For example, ` #[DateTimeImmutableNormalizer] ` . This tells the Hydrator to normalize or denormalize this property.
135+ The library attempts to independently determine which normalizers should be used.
136+ For this purpose, normalizers of this order are determined:
80137
81- Fortunately, we don't have to do this everywhere.
82- The library tries to independently recognize which normalizers are needed based on the data type.
83- For example, if you specify DateTimeImmutable Type, the DateTimeImmutableNormalizer is automatically added.
84- You can of course override this if you want.
85- This makes sense, for example, if you want to adjust the format of the normalized string.
86- You can do this by passing parameters to the normalizer.
138+ 1 ) Does the class property have a normalizer as an attribute? Use this.
139+ 2 ) The data type of the property is determined.
140+ 1 ) If it is a collection, use the ArrayNormalizer (recursive).
141+ 2 ) If it is an object, then look for a normalizer as attribute on the class or interfaces and use this.
142+ 3 ) If it is an object, then guess the normalizer based on the object. Fallback to the object normalizer.
143+
144+ The normalizer is only determined once because it is cached in the metadata.
145+ Below you will find the list of all normalizers and how to set them manually or explicitly.
87146
88147#### Array
89148
90- If you have a list of objects that you want to normalize, then you must normalize each object individually.
91- That's what the ` ArrayNormalizer ` does for you.
92- In order to use the ` ArrayNormaliser ` , you still have to specify which normaliser should be applied to the individual
93- objects. Internally, it basically does an ` array_map ` and then runs the specified normalizer on each element.
149+ If you have a collection (array, iterable, list) with a data type that needs to be normalized,
150+ you can use the ArrayNormalizer and pass it the required normalizer.
94151
95152``` php
96153use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
@@ -183,7 +240,6 @@ final class DTO
183240#### Enum
184241
185242Backed enums can also be normalized.
186- For this, the enum FQCN must also be pass so that the ` EnumNormalizer ` knows which enum it is.
187243
188244``` php
189245use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
@@ -252,14 +308,14 @@ final class Name
252308
253309For this we now need a custom normalizer.
254310This normalizer must implement the ` Normalizer ` interface.
255- You also need to implement a ` normalize ` and ` denormalize ` method.
256- Finally, you have to allow the normalizer to be used as an attribute .
311+ Finally, you have to allow the normalizer to be used as an attribute,
312+ best to allow it for properties as well as classes .
257313
258314``` php
259315use Patchlevel\Hydrator\Normalizer\Normalizer;
260316use Patchlevel\Hydrator\Normalizer\InvalidArgument;
261317
262- #[Attribute(Attribute::TARGET_PROPERTY)]
318+ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS )]
263319class NameNormalizer implements Normalizer
264320{
265321 public function normalize(mixed $value): string
@@ -286,9 +342,6 @@ class NameNormalizer implements Normalizer
286342}
287343```
288344
289- > [ !WARNING]
290- > The important thing is that the result of Normalize is serializable!
291-
292345Now we can also use the normalizer directly.
293346
294347``` php
@@ -301,40 +354,48 @@ final class DTO
301354
302355### Define normalizer on class level
303356
304- You can also set the attribute on the value object on class level.
305- For that the normalizer needs to allow to be set on class level.
357+ Instead of specifying the normalizer on each property, you can also set the normalizer on the class or on an interface.
306358
307359``` php
308- use Patchlevel\Hydrator\Normalizer\Normalizer;
309- use Patchlevel\Hydrator\Normalizer\InvalidArgument;
310-
311- #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
312- class NameNormalizer implements Normalizer
360+ #[NameNormalizer]
361+ final class Name
313362{
314363 // ... same as before
315364}
316365```
317366
318- Then set the attribute on the value object.
367+ ### Guess normalizer
319368
369+ It's also possible to write your own guesser that finds the correct normalizer based on the object.
370+ This is useful if, for example, setting the normalizer on the class or interface isn't possible.
320371
321372``` php
322- #[NameNormalizer]
323- final class Name
373+ use Patchlevel\Hydrator\Guesser\Guesser;
374+ use Symfony\Component\TypeInfo\Type\ObjectType;
375+
376+ class NameGuesser implements Guesser
324377{
325- // ... same as before
378+ public function guess(ObjectType $object): Normalizer|null
379+ {
380+ return match($object->getClassName()) {
381+ case Name::class => new NameNormalizer(),
382+ default => null,
383+ };
384+ }
326385}
327386```
328387
329- After that the DTO can then look like this.
388+ To use this Guesser, you must specify it when creating the Hydrator:
330389
331390``` php
332- final class DTO
333- {
334- public Name $name
335- }
391+ use Patchlevel\Hydrator\MetadataHydrator;
392+
393+ $hydrator = MetadataHydrator::create([new NameGuesser()]);
336394```
337395
396+ > [ !NOTE]
397+ > The guessers are queried in order, and the first match is returned. Finally, our built-in guesser is executed.
398+
338399### Normalized Name
339400
340401By default, the property name is used to name the field in the normalized result.
0 commit comments