Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ static: phpstan cs ## run stat
test: phpunit ## run tests

.PHONY: snapshot
snapshot: vendor ## regenerate the snapshot of the generated middleware
snapshot: vendor ## regenerate the snapshots of the generated transformers
UPDATE_SNAPSHOTS=1 vendor/bin/phpunit --no-coverage --filter testGeneratedCodeMatchesSnapshot

.PHONY: benchmark
Expand Down
65 changes: 52 additions & 13 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,20 @@ The library ships with six extensions out of the box:

| Extension | Purpose |
| --- | --- |
| `CoreExtension` | The default behaviour, the `TransformMiddleware` and the `BuiltInGuesser`. |
| `CoreExtension` | The default behaviour, the `BuiltInGuesser` for enums, dates and nested objects. |
| `LifecycleExtension` | [Lifecycle hooks](lifecycle-hooks.md), run code before and after the extract and hydrate process. |
| `CryptographyExtension` | [Cryptography](cryptography.md), encrypt and decrypt sensitive data with crypto-shredding. |
| `UpcastExtension` | [Upcasting](upcasting.md), reshape outdated stored data while it is hydrated. |
| `TracingExtension` | [Tracing](tracing.md), measure every hydrate and extract call. |
| `GeneratedMiddlewareExtension` | [Generated code](generated-code.md), generated mapping code for a fixed set of classes on top of the `CoreExtension`. |
| `GeneratedTransformerExtension` | [Generated code](generated-code.md), generated mapping code instead of reflection on top of the `CoreExtension`. |

## Middleware

A middleware wraps the hydration and extraction process, similar to HTTP
middlewares. It can modify the incoming data, the outgoing array or the object
itself, and then delegates to the next middleware on the stack. The innermost
middleware is the `TransformMiddleware`, which does the actual property mapping.
itself, and then delegates to the next middleware on the stack. At the end of
the stack the hydrator does the actual property mapping with a
[transformer](#transformer-factories).

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

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

:::warning
At least one middleware has to run. If every middleware skips a class, an
`AllMiddlewaresSkipped` exception is thrown.
:::tip
If every middleware skips a class, the hydrator calls the transformer directly
without building the middleware stack.
:::

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

## Transformer factories

At the end of the stack the hydrator turns the array into the object and back
with the `ClassTransformer` of the class. By default this is the
`ReflectionTransformer`.
A transformer factory can provide another transformer for a class, the
[generated code](generated-code.md) extension uses this to replace reflection
with generated code.

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

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

return new MoneyTransformer();
}
}
```
```php
$builder->addTransformerFactory(new MoneyTransformerFactory());
```
The factories are asked once per class, the first transformer wins and is
cached by the hydrator. If no factory returns a transformer, reflection is
used. Factories also accept a priority, a factory with a higher priority is
asked first.

:::tip
If no middleware has to run for a class, because there is none or all of them
[skip](#skipping-middlewares) it, the hydrator calls the transformer directly
without building the middleware stack.
:::

## Decorators

A decorator wraps the whole hydrator instead of a single step inside the
stack. Use it for things which concern the call as a whole, like
[tracing](tracing.md) or logging. A decorator implements `HydratorDecorator`
and returns a new `Hydrator` around the given one. It also receives the
innermost `StackHydrator`, in case it needs its metadata.
and returns a new `Hydrator` around the given one.

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\HydratorDecorator;
use Patchlevel\Hydrator\StackHydrator;

final class AuditDecorator implements HydratorDecorator
{
Expand All @@ -174,7 +213,7 @@ final class AuditDecorator implements HydratorDecorator
) {
}

public function decorate(Hydrator $hydrator, StackHydrator $stack): Hydrator
public function decorate(Hydrator $hydrator): Hydrator
{
return new AuditHydrator($hydrator, $this->auditLog);
}
Expand Down
125 changes: 59 additions & 66 deletions docs/generated-code.md
Original file line number Diff line number Diff line change
@@ -1,113 +1,106 @@
# Generated Code

The `TransformMiddleware` maps arrays to objects with reflection: it iterates
over the metadata of a class, sets each property through a `ReflectionProperty`
and calls the [normalizers](normalizer.md) dynamically. This is fast enough for
most applications, but when you hydrate millions of objects the reflection
overhead adds up. The `GeneratedMiddlewareExtension` generates plain PHP code
for a fixed set of classes, which is then used instead of the reflection based
mapping for exactly these classes.
By default the `ReflectionTransformer` maps arrays to objects with reflection:
it iterates over the metadata of a class, sets each property through a
`ReflectionProperty` and calls the [normalizers](normalizer.md) dynamically.
This is fast enough for most applications, but when you hydrate millions of
objects the reflection overhead adds up. The `GeneratedTransformerExtension`
uses generated plain PHP code instead, one class per hydrated class.

## Enable the extension

Register the `GeneratedMiddlewareExtension` in addition to the `CoreExtension`.
It needs a writable directory for the generated file and the list of classes
for which code should be generated. Classes referenced by these classes, such
as nested value objects, are discovered automatically.
Register the `GeneratedTransformerExtension` in addition to the `CoreExtension`.
It needs a directory where the generated transformers are stored.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension;
use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new GeneratedMiddlewareExtension(
->useExtension(new GeneratedTransformerExtension(
cachePath: __DIR__ . '/var/cache/hydrator',
classes: [
ProfileCreated::class,
NameChanged::class,
],
autoGenerate: $debug,
))
->buildHydrator();
```
:::note
The extension registers a [decorator](extensions.md#decorators) around the
`StackHydrator`. For the generated classes it calls the generated code directly
and skips the middleware stack entirely, as long as no other middleware has to
run for the class (see [Nested objects](#nested-objects) for how that is
decided). Everything else is passed on to the `StackHydrator`. The decorator
has the lowest priority, so other decorators like [tracing](tracing.md) are
always wrapped around it.
:::

:::warning
The extension requires `buildHydrator()`. The deprecated `build()` can not
apply decorators and throws a `DecoratorsNotApplied` exception.
:::
The extension registers a [transformer factory](extensions.md#transformer-factories),
nothing else changes: all middlewares run as before and the generated code
takes the place of reflection at the end of the stack. If no other middleware
has to run for a class, the hydrator calls the generated code directly.

:::success
The generated code behaves exactly like the `TransformMiddleware`: objects are
The generated code behaves exactly like the `ReflectionTransformer`: objects are
created without calling the constructor, missing fields keep their promoted
default value, private and readonly properties are supported and the same
exceptions are thrown. You can switch between both without changing your
classes.
:::

The generated middleware runs right before the `TransformMiddleware` of the
`CoreExtension`, regardless of the registration order. The code is generated
when the hydrator is built, so it always matches the metadata of all registered
extensions. Classes which are not part of the list are passed on and handled by
the `TransformMiddleware` as usual.
## Generate the code

## Caching

The generated middleware is written to the cache path once and reused on
subsequent requests, so the generation only happens on the first run. The file
name is derived from the configuration, changing the class list or the options
results in a new file. In development you can regenerate the code on every
build with the `debug` flag.
With `autoGenerate` the transformer of a class is generated the first time the
class is hydrated or extracted. This is convenient in development, but in
production the cache path is often read-only. There you generate the code
ahead of time, for example when the application is deployed, with the
`TransformerCompiler`. Use the metadata factory of the builder, so the code is
generated with the guessers and enrichers of all extensions.

```php
use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension;
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension;
use Patchlevel\Hydrator\Extension\Generated\TransformerCompiler;
use Patchlevel\Hydrator\StackHydratorBuilder;

$extension = new GeneratedMiddlewareExtension(
cachePath: __DIR__ . '/var/cache/hydrator',
classes: [ProfileCreated::class],
debug: true,
);
$builder = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new GeneratedTransformerExtension(__DIR__ . '/var/cache/hydrator'));

$compiler = new TransformerCompiler($builder->metadataFactory(), __DIR__ . '/var/cache/hydrator');
$compiler->compile([
ProfileCreated::class,
NameChanged::class,
]);
```
:::warning
The generated code is tied to the [metadata](caching.md) and the definition of
your classes. If a field is renamed, a normalizer is added, a property changes
its visibility or a default value changes after the code was generated, the
middleware throws an `OutdatedGeneratedMiddleware` exception instead of running
the outdated code. Clear the cache path when you deploy, like you do for other
generated files.
Classes referenced by these classes, such as nested value objects, are
discovered and generated automatically.

:::note
Classes without generated code are transformed with reflection. A missing
compile step makes the hydrator slower, but never breaks it.
:::

## Outdated code

The generated code is tied to the [metadata](caching.md) and the definition of
your classes. The name of a generated transformer contains a fingerprint of
the class: its properties, field names, normalizers, visibility and defaults.
If one of them changes, the existing code no longer matches and the class is
transformed with reflection until its code is generated again. Old files are
never used again, you can delete them when you deploy.

## Nested objects

The biggest speedup comes from inlining nested objects: instead of going through
the whole middleware stack again for every nested object, the generated code
hydrates and extracts them directly. Inlining a class would hide it from the
the hydrator again for every nested object, the generated code hydrates and
extracts them directly. Inlining a class would hide it from the
other middlewares, so it only happens when every other middleware in the stack
is a `SkippableMiddleware` which [skips](extensions.md) that class. The decision
is made per class and per direction: with the
is a `SkippableMiddleware` which [skips](extensions.md#skipping-middlewares)
that class. The decision is made per class and per direction: with the
[cryptography](cryptography.md) extension for example, value objects without
sensitive data are inlined while classes with sensitive data still go through
the stack.

Inlining would also hide nested objects from [decorators](extensions.md#decorators),
so nothing is inlined as soon as another decorator is registered. With the
so nothing is inlined as soon as a decorator is registered. With the
[tracing](tracing.md) extension, every nested object goes through the tracer
and shows up in the traces, at the cost of the inlining.

:::note
Nested classes with a class level normalizer or [lazy](lazy.md) classes are
never inlined, they always take the regular path through the hydrator. The
generated middleware is itself skippable: classes it has no code for go
straight to the `TransformMiddleware`.
Nested classes with a class level normalizer, [lazy](lazy.md) classes and
nested classes which changed since the code was generated are never inlined,
they always take the regular path through the hydrator.
:::

## Learn more
Expand Down
7 changes: 3 additions & 4 deletions docs/hydrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ resolves [normalizers](normalizer.md) from metadata.
## Create the hydrator

The recommended way is the `StackHydratorBuilder` with the `CoreExtension`.
The `CoreExtension` registers the `TransformMiddleware`, which does the actual
property mapping, and the `BuiltInGuesser`, which picks normalizers for enums,
date types and nested objects.
The `CoreExtension` registers the `BuiltInGuesser`, which picks normalizers for
enums, date types and nested objects.

```php
use Patchlevel\Hydrator\CoreExtension;
Expand All @@ -28,7 +27,7 @@ works as long as no decorator is registered.
:::

If you don't need any extensions, you can also instantiate the `StackHydrator`
directly, it defaults to the same middleware and guesser:
directly, it behaves like the builder with only the `CoreExtension`:

```php
use Patchlevel\Hydrator\StackHydrator;
Expand Down
12 changes: 6 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ parameters:
path: src/Metadata/ClassMetadata.php

-
message: '#^Method Patchlevel\\Hydrator\\Middleware\\TransformMiddleware\:\:hydrate\(\) should return T of object but returns mixed\.$#'
message: '#^Method Patchlevel\\Hydrator\\Transformer\\ReflectionTransformer\:\:hydrate\(\) should return T of object but returns mixed\.$#'
identifier: return.type
count: 1
path: src/Middleware/TransformMiddleware.php
path: src/Transformer/ReflectionTransformer.php

-
message: '#^Parameter \#1 \$object of method Patchlevel\\Hydrator\\Metadata\\PropertyMetadata\:\:setValue\(\) expects object, mixed given\.$#'
identifier: argument.type
count: 2
path: src/Middleware/TransformMiddleware.php
path: src/Transformer/ReflectionTransformer.php

-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
Expand All @@ -121,9 +121,9 @@ parameters:
path: src/Normalizer/ObjectNormalizer.php

-
message: '#^Method Patchlevel\\Hydrator\\StackHydrator\:\:middlewaresFor\(\) should return non\-empty\-list\<Patchlevel\\Hydrator\\Middleware\\Middleware\> but returns list\<Patchlevel\\Hydrator\\Middleware\\Middleware\>\.$#'
identifier: return.type
count: 1
message: '#^Parameter \#1 \$data of method Patchlevel\\Hydrator\\Transformer\\ClassTransformer\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
identifier: argument.type
count: 3
path: src/StackHydrator.php

-
Expand Down
2 changes: 0 additions & 2 deletions src/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
namespace Patchlevel\Hydrator;

use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
use Patchlevel\Hydrator\Middleware\TransformMiddleware;

final class CoreExtension implements Extension
{
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMiddleware(new TransformMiddleware(), Extension::PRIORITY_TRANSFORM);
$builder->addGuesser(new BuiltInGuesser(), -64);
}
}
6 changes: 5 additions & 1 deletion src/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ interface Extension
/** Last structural step before the array becomes an object. */
public const PRIORITY_BEFORE_TRANSFORM = 0;

/** Build the object from the array and deconstruct it again. */
/**
* Build the object from the array and deconstruct it again.
*
* @deprecated the hydrator transforms the data at the end of the stack, use a transformer factory to change it
*/
public const PRIORITY_TRANSFORM = -64;

public function configure(StackHydratorBuilder $builder): void;
Expand Down
Loading
Loading