Skip to content

Commit 1c413c3

Browse files
authored
Merge pull request #217 from patchlevel/class-transformer
Map the data with class transformers and remove the CoreExtension
2 parents 81e69af + 053434d commit 1c413c3

41 files changed

Lines changed: 644 additions & 338 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/UPGRADE-3.0.md‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@ searchable: false
33
---
44
# Upgrade 3.0
55

6+
## Builder
7+
8+
### CoreExtension
9+
10+
The `CoreExtension` has been removed. The property mapping and the
11+
`BuiltInGuesser` are now always part of the hydrator, remove the registration.
12+
13+
before:
14+
15+
```php
16+
use Patchlevel\Hydrator\CoreExtension;
17+
use Patchlevel\Hydrator\StackHydratorBuilder;
18+
19+
$hydrator = (new StackHydratorBuilder())
20+
->useExtension(new CoreExtension())
21+
->build();
22+
```
23+
after:
24+
25+
```php
26+
use Patchlevel\Hydrator\StackHydratorBuilder;
27+
28+
$hydrator = (new StackHydratorBuilder())
29+
->build();
30+
```
31+
The `BuiltInGuesser` is always asked after all guessers you register with
32+
`addGuesser()`, regardless of their priority.
33+
634
## Normalizer
735

836
### HydratorAwareNormalizer
@@ -207,7 +235,7 @@ final class AuditMiddleware implements Middleware
207235
}
208236
```
209237
In tests, where a middleware is called on its own, pass a `Next` with the
210-
following middlewares instead of a `Stack`.
238+
following middlewares and the transformer instead of a `Stack`.
211239

212240
before:
213241

@@ -221,10 +249,42 @@ after:
221249

222250
```php
223251
use Patchlevel\Hydrator\Middleware\Next;
252+
use Patchlevel\Hydrator\Transformer\ReflectionTransformer;
253+
254+
$object = $middleware->hydrate($metadata, $data, [], new Next([], new ReflectionTransformer($metadata)));
255+
```
256+
### TransformMiddleware
257+
258+
The `TransformMiddleware` has been removed. The hydrator maps the data with a
259+
`ClassTransformer` after all middlewares, the `ReflectionTransformer` by
260+
default. Remove the `TransformMiddleware` if you pass the middlewares to the
261+
`StackHydrator` yourself. `Extension::PRIORITY_TRANSFORM` has been removed as
262+
well.
263+
264+
before:
265+
266+
```php
224267
use Patchlevel\Hydrator\Middleware\TransformMiddleware;
268+
use Patchlevel\Hydrator\StackHydrator;
225269

226-
$object = $middleware->hydrate($metadata, $data, [], new Next([new TransformMiddleware()]));
270+
$hydrator = new StackHydrator(middlewares: [new AuditMiddleware(), new TransformMiddleware()]);
227271
```
272+
after:
273+
274+
```php
275+
use Patchlevel\Hydrator\StackHydrator;
276+
277+
$hydrator = new StackHydrator(middlewares: [new AuditMiddleware()]);
278+
```
279+
If you replaced the `TransformMiddleware` with your own mapping, implement a
280+
`ClassTransformerFactory` instead and register it with
281+
`StackHydratorBuilder::addTransformerFactory()`.
282+
283+
### Removed exceptions
284+
285+
`MissingMiddlewares`, `AllMiddlewaresSkipped` and `NoMoreMiddleware` have been
286+
removed. A hydrator without middlewares is valid, and a class which every
287+
middleware skips is only transformed. Remove the `catch` blocks for them.
228288
## Caching
229289

230290
The serialized form of the normalizers changed. Clear the

‎docs/caching.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ Pass the cache to the builder with `setCache`. Both PSR-6
1313
implementations are accepted.
1414

1515
```php
16-
use Patchlevel\Hydrator\CoreExtension;
1716
use Patchlevel\Hydrator\StackHydratorBuilder;
1817
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
1918

2019
$hydrator = (new StackHydratorBuilder())
21-
->useExtension(new CoreExtension())
2220
->setCache(new FilesystemAdapter())
2321
->build();
2422
```

‎docs/cryptography.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ Register the `CryptographyExtension` on the builder and pass it a
1616
choice; it needs a [cipher key store](#cipher-key-store) to keep the keys.
1717

1818
```php
19-
use Patchlevel\Hydrator\CoreExtension;
2019
use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer;
2120
use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension;
2221
use Patchlevel\Hydrator\Extension\Cryptography\Store\InMemoryCipherKeyStore;
@@ -25,7 +24,6 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
2524
$cipherKeyStore = new InMemoryCipherKeyStore();
2625

2726
$hydrator = (new StackHydratorBuilder())
28-
->useExtension(new CoreExtension())
2927
->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($cipherKeyStore)))
3028
->build();
3129
```

‎docs/extensions.md‎

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
# Extensions
22

33
The `StackHydrator` is assembled from small building blocks: middlewares that
4-
wrap the hydration process, [guessers](guesser.md) that resolve normalizers and
5-
metadata enrichers that add information to the class metadata. An extension
6-
bundles such building blocks so they can be registered with a single call.
4+
wrap the hydration process, [guessers](guesser.md) that resolve normalizers,
5+
metadata enrichers that add information to the class metadata and transformer
6+
factories that map the data to the object. An extension bundles such building
7+
blocks so they can be registered with a single call.
78

89
## Using extensions
910

1011
Extensions are registered on the `StackHydratorBuilder` with `useExtension`.
11-
The `CoreExtension` provides the default behaviour and should (almost) always
12-
be there.
12+
The default behaviour, the property mapping and the
13+
[built-in guesser](guesser.md#built-in-guesser), is always there, you don't
14+
have to register anything for it.
1315

1416
```php
15-
use Patchlevel\Hydrator\CoreExtension;
1617
use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension;
1718
use Patchlevel\Hydrator\StackHydratorBuilder;
1819

1920
$hydrator = (new StackHydratorBuilder())
20-
->useExtension(new CoreExtension())
2121
->useExtension(new LifecycleExtension())
2222
->build();
2323
```
2424
## Built-in extensions
2525

26-
The library ships with four extensions out of the box:
26+
The library ships with three extensions out of the box:
2727

2828
| Extension | Purpose |
2929
| --- | --- |
30-
| `CoreExtension` | The default behaviour, the `TransformMiddleware` and the `BuiltInGuesser`. |
3130
| `LifecycleExtension` | [Lifecycle hooks](lifecycle-hooks.md), run code before and after the extract and hydrate process. |
3231
| `CryptographyExtension` | [Cryptography](cryptography.md), encrypt and decrypt sensitive data with crypto-shredding. |
3332
| `UpcastExtension` | [Upcasting](upcasting.md), reshape outdated stored data while it is hydrated. |
@@ -36,8 +35,9 @@ The library ships with four extensions out of the box:
3635

3736
A middleware wraps the hydration and extraction process, similar to HTTP
3837
middlewares. It can modify the incoming data, the outgoing array or the object
39-
itself, and passes the call on to the rest of the stack with `$next`. The innermost
40-
middleware is the `TransformMiddleware`, which does the actual property mapping.
38+
itself, and passes the call on to the rest of the stack with `$next`. After the last
39+
middleware the [transformer](#transformer-factories) does the actual property
40+
mapping.
4141

4242
```php
4343
use Patchlevel\Hydrator\Metadata\ClassMetadata;
@@ -60,8 +60,7 @@ final class RemoveNullValuesMiddleware implements Middleware
6060
}
6161
```
6262
Middlewares are added with a priority, higher priorities run first (outermost).
63-
The `TransformMiddleware` from the `CoreExtension` has priority `-64`, so it
64-
always runs last.
63+
The transformation always runs last, after all middlewares.
6564

6665
```php
6766
$builder->addMiddleware(new RemoveNullValuesMiddleware(), 0);
@@ -113,9 +112,9 @@ upcaster is a `CallbackUpcaster` or carries an
113112
[`#[UpcasterFor]`](upcasting.md#writing-an-upcaster) attribute, it is also left
114113
out while hydrating classes none of them target.
115114

116-
:::warning
117-
At least one middleware has to run. If every middleware skips a class, an
118-
`AllMiddlewaresSkipped` exception is thrown.
115+
:::tip
116+
If every middleware skips a class, the hydrator calls the transformer directly
117+
without building the middleware stack.
119118
:::
120119

121120
## Metadata enricher
@@ -152,6 +151,39 @@ extras) can be [cached](caching.md), everything you store in `extras` must be
152151
serializable.
153152
:::
154153

154+
## Transformer factories
155+
156+
At the end of the stack a `ClassTransformer` turns the array into the object
157+
and back. By default this is the `ReflectionTransformer`, which sets and reads
158+
the properties with reflection. A transformer factory can provide another
159+
transformer for a class, for example generated code.
160+
161+
```php
162+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
163+
use Patchlevel\Hydrator\Transformer\ClassTransformer;
164+
use Patchlevel\Hydrator\Transformer\ClassTransformerFactory;
165+
166+
final class MoneyTransformerFactory implements ClassTransformerFactory
167+
{
168+
public function create(ClassMetadata $metadata): ClassTransformer|null
169+
{
170+
if ($metadata->className !== Money::class) {
171+
return null;
172+
}
173+
174+
return new MoneyTransformer();
175+
}
176+
}
177+
```
178+
```php
179+
$builder->addTransformerFactory(new MoneyTransformerFactory());
180+
```
181+
The factories are asked once per class, the first transformer wins and is
182+
cached by the hydrator. Factories also accept a priority, a factory with a
183+
higher priority is asked first. The `ReflectionTransformerFactory` is always
184+
asked last, so every class without its own transformer is transformed with
185+
reflection.
186+
155187
## Writing your own extension
156188

157189
An extension implements the `Extension` interface and configures the builder.

‎docs/getting-started.md‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,13 @@ the collection. How types are resolved into normalizers is explained on the
4747

4848
## Create the hydrator
4949

50-
The recommended way to create a hydrator is the `StackHydratorBuilder` together
51-
with the `CoreExtension`, which registers the default middleware and the
52-
built-in normalizer guesser.
50+
The recommended way to create a hydrator is the `StackHydratorBuilder`. It
51+
already comes with the property mapping and the built-in normalizer guesser.
5352

5453
```php
55-
use Patchlevel\Hydrator\CoreExtension;
5654
use Patchlevel\Hydrator\StackHydratorBuilder;
5755

5856
$hydrator = (new StackHydratorBuilder())
59-
->useExtension(new CoreExtension())
6057
->build();
6158
```
6259
:::note

‎docs/guesser.md‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class itself, for example for third-party classes.
77

88
## Built-in guesser
99

10-
The `BuiltInGuesser` is registered by the `CoreExtension`. It resolves backed
10+
The `BuiltInGuesser` is always registered. It resolves backed
1111
enums to the `EnumNormalizer`, the date types (`DateTimeImmutable`, `DateTime`,
1212
`DateTimeZone`, `DateInterval`) to their normalizers and falls back to the
1313
`ObjectNormalizer` for everything else.
@@ -36,18 +36,16 @@ final class NameGuesser implements Guesser
3636
To use the guesser, add it to the builder:
3737

3838
```php
39-
use Patchlevel\Hydrator\CoreExtension;
4039
use Patchlevel\Hydrator\StackHydratorBuilder;
4140

4241
$hydrator = (new StackHydratorBuilder())
43-
->useExtension(new CoreExtension())
4442
->addGuesser(new NameGuesser())
4543
->build();
4644
```
4745
:::note
4846
The guessers are queried in order of their priority, and the first match wins.
49-
The built-in guesser is registered with priority `-64`, so your own guessers run
50-
before the fallback to the `ObjectNormalizer`.
47+
The built-in guesser is always asked last, so your own guessers run before the
48+
fallback to the `ObjectNormalizer`.
5149
:::
5250

5351
## Mapped guesser
@@ -56,12 +54,10 @@ For the common case of a simple class-to-normalizer mapping, you don't need to
5654
write your own guesser class, use the `MappedGuesser`:
5755

5856
```php
59-
use Patchlevel\Hydrator\CoreExtension;
6057
use Patchlevel\Hydrator\Guesser\MappedGuesser;
6158
use Patchlevel\Hydrator\StackHydratorBuilder;
6259

6360
$hydrator = (new StackHydratorBuilder())
64-
->useExtension(new CoreExtension())
6561
->addGuesser(new MappedGuesser([
6662
Name::class => NameNormalizer::class,
6763
Email::class => EmailNormalizer::class,

‎docs/hydrator.md‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,18 @@ resolves [normalizers](normalizer.md) from metadata.
77

88
## Create the hydrator
99

10-
The recommended way is the `StackHydratorBuilder` with the `CoreExtension`.
11-
The `CoreExtension` registers the `TransformMiddleware`, which does the actual
12-
property mapping, and the `BuiltInGuesser`, which picks normalizers for enums,
13-
date types and nested objects.
10+
The recommended way is the `StackHydratorBuilder`. It maps the properties with
11+
reflection and comes with the `BuiltInGuesser`, which picks normalizers for
12+
enums, date types and nested objects.
1413

1514
```php
16-
use Patchlevel\Hydrator\CoreExtension;
1715
use Patchlevel\Hydrator\StackHydratorBuilder;
1816

1917
$hydrator = (new StackHydratorBuilder())
20-
->useExtension(new CoreExtension())
2118
->build();
2219
```
2320
If you don't need any extensions, you can also instantiate the `StackHydrator`
24-
directly, it defaults to the same middleware and guesser:
21+
directly, it behaves the same as a hydrator built by the builder:
2522

2623
```php
2724
use Patchlevel\Hydrator\StackHydrator;

‎docs/lazy.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,9 @@ Instead of marking every class, you can make lazy hydration the default when
3434
building the hydrator.
3535

3636
```php
37-
use Patchlevel\Hydrator\CoreExtension;
3837
use Patchlevel\Hydrator\StackHydratorBuilder;
3938

4039
$hydrator = (new StackHydratorBuilder())
41-
->useExtension(new CoreExtension())
4240
->enableDefaultLazy()
4341
->build();
4442
```

‎docs/lifecycle-hooks.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ result. For this, the `LifecycleExtension` provides four method attributes:
1010
Register the `LifecycleExtension` on the builder:
1111

1212
```php
13-
use Patchlevel\Hydrator\CoreExtension;
1413
use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension;
1514
use Patchlevel\Hydrator\StackHydratorBuilder;
1615

1716
$hydrator = (new StackHydratorBuilder())
18-
->useExtension(new CoreExtension())
1917
->useExtension(new LifecycleExtension())
2018
->build();
2119
```

‎docs/upcasting.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ Register the `UpcastExtension` on the builder and pass it a list of upcasters.
1111
Each upcaster receives the raw data array and returns a reshaped array.
1212

1313
```php
14-
use Patchlevel\Hydrator\CoreExtension;
1514
use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
1615
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;
1716
use Patchlevel\Hydrator\StackHydratorBuilder;
1817

1918
$hydrator = (new StackHydratorBuilder())
20-
->useExtension(new CoreExtension())
2119
->useExtension(new UpcastExtension(
2220
beforeTransform: [
2321
CallbackUpcaster::forClass(

0 commit comments

Comments
 (0)