Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 86 additions & 22 deletions docs/UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,14 @@ final class LoggingHydrator implements Hydrator
```
## Middleware

### Next instead of Stack
### Mapper instead of Stack

`Patchlevel\Hydrator\Middleware\Stack` has been removed in favor of
`Patchlevel\Hydrator\Middleware\Next`. A middleware no longer asks the stack for
the next middleware and calls it with the stack again, it calls `$next`
directly with the metadata, the data and the context. Change the last parameter
of `hydrate()` and `extract()` in your middlewares to `Next $next`.
`Patchlevel\Hydrator\Middleware\Stack` has been removed. The hydrator builds
the chain of a class once: the mapper of the class, wrapped in the middlewares
which run for it. A middleware receives the rest of this chain as a
`Patchlevel\Hydrator\Mapper\Mapper` and calls it with the data or the object
and the context, the metadata is no longer passed on. Change the last parameter
of `hydrate()` and `extract()` in your middlewares to `Mapper $next`.

before:

Expand All @@ -217,25 +218,25 @@ final class AuditMiddleware implements Middleware
after:

```php
use Patchlevel\Hydrator\Mapper\Mapper;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Next;

final class AuditMiddleware implements Middleware
{
public function hydrate(ClassMetadata $metadata, array $data, array $context, Next $next): object
public function hydrate(ClassMetadata $metadata, array $data, array $context, Mapper $next): object
{
return $next->hydrate($metadata, $data, $context);
return $next->hydrate($data, $context);
}

public function extract(ClassMetadata $metadata, object $object, array $context, Next $next): array
public function extract(ClassMetadata $metadata, object $object, array $context, Mapper $next): array
{
return $next->extract($metadata, $object, $context);
return $next->extract($object, $context);
}
}
```
In tests, where a middleware is called on its own, pass a `Next` with the
following middlewares and the transformer instead of a `Stack`.
In tests, where a middleware is called on its own, pass a `ReflectionMapper`
of the class instead of a `Stack`.

before:

Expand All @@ -248,16 +249,15 @@ $object = $middleware->hydrate($metadata, $data, [], new Stack([new TransformMid
after:

```php
use Patchlevel\Hydrator\Middleware\Next;
use Patchlevel\Hydrator\Transformer\ReflectionTransformer;
use Patchlevel\Hydrator\Mapper\ReflectionMapper;

$object = $middleware->hydrate($metadata, $data, [], new Next([], new ReflectionTransformer($metadata)));
$object = $middleware->hydrate($metadata, $data, [], new ReflectionMapper($metadata));
```
### TransformMiddleware

The `TransformMiddleware` has been removed. The hydrator maps the data with a
`ClassTransformer` after all middlewares, the `ReflectionTransformer` by
default. Remove the `TransformMiddleware` if you pass the middlewares to the
The `TransformMiddleware` has been removed. The hydrator maps the data with the
mapper of the class after all middlewares, the `ReflectionMapper` by default.
Remove the `TransformMiddleware` if you pass the middlewares to the
`StackHydrator` yourself. `Extension::PRIORITY_TRANSFORM` has been removed as
well.

Expand All @@ -277,14 +277,78 @@ use Patchlevel\Hydrator\StackHydrator;
$hydrator = new StackHydrator(middlewares: [new AuditMiddleware()]);
```
If you replaced the `TransformMiddleware` with your own mapping, implement a
`ClassTransformerFactory` instead and register it with
`StackHydratorBuilder::addTransformerFactory()`.
`Patchlevel\Hydrator\Mapper\MapperFactory` instead and register it with
`StackHydratorBuilder::addMapperFactory()`.

```php
use Patchlevel\Hydrator\Mapper\Mapper;
use Patchlevel\Hydrator\Mapper\MapperFactory;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\StackHydrator;
use Patchlevel\Hydrator\StackHydratorBuilder;

final class MoneyMapperFactory implements MapperFactory
{
public function create(ClassMetadata $metadata, StackHydrator $hydrator): Mapper|null
{
if ($metadata->className !== Money::class) {
return null;
}

return new MoneyMapper();
}
}

$hydrator = (new StackHydratorBuilder())
->addMapperFactory(new MoneyMapperFactory())
->build();
```
### PRIORITY_BEFORE_TRANSFORM

`Extension::PRIORITY_BEFORE_TRANSFORM` has been renamed to
`Extension::PRIORITY_BEFORE_MAPPING`, the value stays `0`.

before:

```php
use Patchlevel\Hydrator\Extension;

$builder->addMiddleware(new AuditMiddleware(), Extension::PRIORITY_BEFORE_TRANSFORM);
```
after:

```php
use Patchlevel\Hydrator\Extension;

$builder->addMiddleware(new AuditMiddleware(), Extension::PRIORITY_BEFORE_MAPPING);
```
### Removed exceptions

`MissingMiddlewares`, `AllMiddlewaresSkipped` and `NoMoreMiddleware` have been
removed. A hydrator without middlewares is valid, and a class which every
middleware skips is only transformed. Remove the `catch` blocks for them.
middleware skips is only mapped. Remove the `catch` blocks for them.

## Upcasting

### beforeTransform

The `beforeTransform` argument of the `UpcastExtension` has been renamed to
`beforeMapping`.

before:

```php
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;

$extension = new UpcastExtension(beforeTransform: [$mergeNameUpcaster]);
```
after:

```php
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;

$extension = new UpcastExtension(beforeMapping: [$mergeNameUpcaster]);
```
## Caching

The serialized form of the normalizers changed. Clear the
Expand Down
95 changes: 60 additions & 35 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

Expand Down Expand Up @@ -33,42 +33,42 @@ The library ships with three extensions out of the box:

## Middleware

A middleware wraps the hydration and extraction process, similar to HTTP
A middleware wraps the [mapper](#mapper-factories) of a class, similar to HTTP
middlewares. It can modify the incoming data, the outgoing array or the object
itself, and passes the call on to the rest of the stack with `$next`. After the last
middleware the [transformer](#transformer-factories) does the actual property
mapping.
itself, and passes the call on with `$next`. `$next` is a `Mapper` too: the
following middlewares and at the end the mapper of the class, which does the
actual property mapping. The hydrator builds this chain once per class.

```php
use Patchlevel\Hydrator\Mapper\Mapper;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Next;

final class RemoveNullValuesMiddleware implements Middleware
{
public function hydrate(ClassMetadata $metadata, array $data, array $context, Next $next): object
public function hydrate(ClassMetadata $metadata, array $data, array $context, Mapper $next): object
{
return $next->hydrate($metadata, $data, $context);
return $next->hydrate($data, $context);
}

public function extract(ClassMetadata $metadata, object $object, array $context, Next $next): array
public function extract(ClassMetadata $metadata, object $object, array $context, Mapper $next): array
{
$data = $next->extract($metadata, $object, $context);
$data = $next->extract($object, $context);

return array_filter($data, static fn (mixed $value) => $value !== null);
}
}
```
Middlewares are added with a priority, higher priorities run first (outermost).
The transformation always runs last, after all middlewares.
The mapper of the class always runs last, after all middlewares.

```php
$builder->addMiddleware(new RemoveNullValuesMiddleware(), 0);
```
## Skipping middlewares

A middleware usually only has something to do for a few classes. Instead of
walking through the whole stack every time, a middleware can implement
being part of the chain of every class, a middleware can implement
`SkippableMiddleware` and tell the hydrator that it is not needed for a class.
The decision is made once per class and then reused, so it must only depend on
the metadata.
Expand All @@ -78,21 +78,21 @@ The `skip` method returns a `Skip` case: `Skip::None` to always run,
`Skip::Both` to be left out completely.

```php
use Patchlevel\Hydrator\Mapper\Mapper;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Next;
use Patchlevel\Hydrator\Middleware\Skip;
use Patchlevel\Hydrator\Middleware\SkippableMiddleware;

final class RemoveNullValuesMiddleware implements SkippableMiddleware
{
public function hydrate(ClassMetadata $metadata, array $data, array $context, Next $next): object
public function hydrate(ClassMetadata $metadata, array $data, array $context, Mapper $next): object
{
return $next->hydrate($metadata, $data, $context);
return $next->hydrate($data, $context);
}

public function extract(ClassMetadata $metadata, object $object, array $context, Next $next): array
public function extract(ClassMetadata $metadata, object $object, array $context, Mapper $next): array
{
$data = $next->extract($metadata, $object, $context);
$data = $next->extract($object, $context);

return array_filter($data, static fn (mixed $value) => $value !== null);
}
Expand All @@ -113,8 +113,8 @@ upcaster is a `CallbackUpcaster` or carries an
out while hydrating classes none of them target.

:::tip
If every middleware skips a class, the hydrator calls the transformer directly
without building the middleware stack.
If every middleware skips a class, the mapper of the class is the whole chain
and the hydrator calls it directly.
:::

## Metadata enricher
Expand Down Expand Up @@ -151,38 +151,63 @@ extras) can be [cached](caching.md), everything you store in `extras` must be
serializable.
:::

## Transformer factories
## Mapper factories

At the end of the stack a `ClassTransformer` turns the array into the object
and back. By default this is the `ReflectionTransformer`, which sets and reads
the properties with reflection. A transformer factory can provide another
transformer for a class, for example generated code.
At the end of the chain the `Mapper` of the class turns the array into the
object and back. By default this is the `ReflectionMapper`, which sets and
reads the properties with reflection. A mapper factory can provide another
mapper for a class, for example generated code.

```php
use Patchlevel\Hydrator\Mapper\Mapper;
use Patchlevel\Hydrator\Mapper\MapperFactory;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Transformer\ClassTransformer;
use Patchlevel\Hydrator\Transformer\ClassTransformerFactory;
use Patchlevel\Hydrator\StackHydrator;

final class MoneyTransformerFactory implements ClassTransformerFactory
final class MoneyMapperFactory implements MapperFactory
{
public function create(ClassMetadata $metadata): ClassTransformer|null
public function create(ClassMetadata $metadata, StackHydrator $hydrator): Mapper|null
{
if ($metadata->className !== Money::class) {
return null;
}

return new MoneyTransformer();
return new MoneyMapper();
}
}
```
```php
$builder->addTransformerFactory(new MoneyTransformerFactory());
$builder->addMapperFactory(new MoneyMapperFactory());
```
The factories are asked once per class, the first transformer wins and is
cached by the hydrator. Factories also accept a priority, a factory with a
higher priority is asked first. The `ReflectionTransformerFactory` is always
asked last, so every class without its own transformer is transformed with
reflection.
The factories are asked once per class, the first mapper wins and is cached by
the hydrator together with its middlewares. Factories also accept a priority, a
factory with a higher priority is asked first. Every class without a mapper of
a factory is mapped with the `ReflectionMapper`.

### Nested objects

`StackHydrator::mapper()` returns the chain the hydrator runs for a class: its
mapper, wrapped in the middlewares and the lazy proxy which run for it. A class
with a class normalizer has no chain, then it returns `null`. A mapper can call
the chain of a nested class directly instead of going through the normalizer
and the hydrator again, the `ReflectionMapper` does this for properties with an
`ObjectNormalizer`.

Only do this if the context contains the hydrator which created the mapper:
a hydrator which wraps it has to see the nested objects.

```php
use Patchlevel\Hydrator\Hydrator;

if (($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator) {
$address = $this->hydrator->mapper(Address::class)?->hydrate($data['address'], $context);
}
```
:::warning
Ask for the mappers of nested classes once the mapper is used, not inside
`create()`. The hydrator caches the mapper only after `create()` returned, so
two classes which reference each other would create each other endlessly.
:::

## Writing your own extension

Expand Down
10 changes: 5 additions & 5 deletions docs/upcasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new UpcastExtension(
beforeTransform: [
beforeMapping: [
CallbackUpcaster::forClass(
ProfileCreated::class,
static function (array $data): array {
Expand Down Expand Up @@ -97,23 +97,23 @@ you pass your upcasters to the matching argument.
| Argument | Runs | Works on |
| --- | --- | --- |
| `beforeEncoding` | before the values are decoded | the raw stored values (strings, ints, ...) |
| `beforeTransform` | after decoding, right before the object is built | the decoded values (enums, dates, value objects, ...) |
| `beforeMapping` | after decoding, right before the object is built | the decoded values (enums, dates, value objects, ...) |

Use `beforeEncoding` when you rename or restructure fields whose raw form is
enough, and `beforeTransform` when you need the already decoded values.
enough, and `beforeMapping` when you need the already decoded values.

```php
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;

$extension = new UpcastExtension(
beforeEncoding: [$renameFieldUpcaster],
beforeTransform: [$mergeNameUpcaster],
beforeMapping: [$mergeNameUpcaster],
);
```
:::warning
The [cryptography](cryptography.md) extension decrypts values during the
decoding stage. A `beforeEncoding` upcaster therefore still sees the encrypted
values, while a `beforeTransform` upcaster sees the decrypted ones. Pick the
values, while a `beforeMapping` upcaster sees the decrypted ones. Pick the
stage that matches the data you need.
:::

Expand Down
Loading
Loading