Skip to content

Commit 32d1114

Browse files
committed
update docs
1 parent e2c4da8 commit 32d1114

File tree

1 file changed

+110
-49
lines changed

1 file changed

+110
-49
lines changed

README.md

Lines changed: 110 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
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
2331
use Patchlevel\Hydrator\MetadataHydrator;
2432

25-
$hydrator = new MetadataHydrator();
33+
$hydrator = MetadataHydrator::create();
2634
```
2735

2836
After 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
3141
final 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.
76133
You 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
96153
use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
@@ -183,7 +240,6 @@ final class DTO
183240
#### Enum
184241

185242
Backed 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
189245
use Patchlevel\Hydrator\Normalizer\EnumNormalizer;
@@ -252,14 +308,14 @@ final class Name
252308

253309
For this we now need a custom normalizer.
254310
This 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
259315
use Patchlevel\Hydrator\Normalizer\Normalizer;
260316
use Patchlevel\Hydrator\Normalizer\InvalidArgument;
261317

262-
#[Attribute(Attribute::TARGET_PROPERTY)]
318+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
263319
class 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-
292345
Now 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

340401
By default, the property name is used to name the field in the normalized result.

0 commit comments

Comments
 (0)