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
64 changes: 62 additions & 2 deletions docs/UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@ searchable: false
---
# Upgrade 3.0

## Builder

### CoreExtension

The `CoreExtension` has been removed. The property mapping and the
`BuiltInGuesser` are now always part of the hydrator, remove the registration.

before:

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->build();
```
after:

```php
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->build();
```
The `BuiltInGuesser` is always asked after all guessers you register with
`addGuesser()`, regardless of their priority.

## Normalizer

### HydratorAwareNormalizer
Expand Down Expand Up @@ -207,7 +235,7 @@ final class AuditMiddleware implements Middleware
}
```
In tests, where a middleware is called on its own, pass a `Next` with the
following middlewares instead of a `Stack`.
following middlewares and the transformer instead of a `Stack`.

before:

Expand All @@ -221,10 +249,42 @@ after:

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

$object = $middleware->hydrate($metadata, $data, [], new Next([], new ReflectionTransformer($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
`StackHydrator` yourself. `Extension::PRIORITY_TRANSFORM` has been removed as
well.

before:

```php
use Patchlevel\Hydrator\Middleware\TransformMiddleware;
use Patchlevel\Hydrator\StackHydrator;

$object = $middleware->hydrate($metadata, $data, [], new Next([new TransformMiddleware()]));
$hydrator = new StackHydrator(middlewares: [new AuditMiddleware(), new TransformMiddleware()]);
```
after:

```php
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()`.

### 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.
## Caching

The serialized form of the normalizers changed. Clear the
Expand Down
2 changes: 0 additions & 2 deletions docs/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ Pass the cache to the builder with `setCache`. Both PSR-6
implementations are accepted.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->setCache(new FilesystemAdapter())
->build();
```
Expand Down
2 changes: 0 additions & 2 deletions docs/cryptography.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Register the `CryptographyExtension` on the builder and pass it a
choice; it needs a [cipher key store](#cipher-key-store) to keep the keys.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer;
use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension;
use Patchlevel\Hydrator\Extension\Cryptography\Store\InMemoryCipherKeyStore;
Expand All @@ -25,7 +24,6 @@ use Patchlevel\Hydrator\StackHydratorBuilder;
$cipherKeyStore = new InMemoryCipherKeyStore();

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($cipherKeyStore)))
->build();
```
Expand Down
64 changes: 48 additions & 16 deletions docs/extensions.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
# Extensions

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

## Using extensions

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

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new LifecycleExtension())
->build();
```
## Built-in extensions

The library ships with four extensions out of the box:
The library ships with three extensions out of the box:

| Extension | Purpose |
| --- | --- |
| `CoreExtension` | The default behaviour, the `TransformMiddleware` and the `BuiltInGuesser`. |
| `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. |
Expand All @@ -36,8 +35,9 @@ The library ships with four extensions out of the box:

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 passes the call on to the rest of the stack with `$next`. The innermost
middleware is the `TransformMiddleware`, which does the actual property mapping.
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.

```php
use Patchlevel\Hydrator\Metadata\ClassMetadata;
Expand All @@ -60,8 +60,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 @@ -113,9 +112,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 @@ -152,6 +151,39 @@ extras) can be [cached](caching.md), everything you store in `extras` must be
serializable.
:::

## Transformer 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.

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

final class MoneyTransformerFactory implements ClassTransformerFactory
{
public function create(ClassMetadata $metadata): 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. 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.

## Writing your own extension

An extension implements the `Extension` interface and configures the builder.
Expand Down
7 changes: 2 additions & 5 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,13 @@ the collection. How types are resolved into normalizers is explained on the

## Create the hydrator

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

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->build();
```
:::note
Expand Down
10 changes: 3 additions & 7 deletions docs/guesser.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class itself, for example for third-party classes.

## Built-in guesser

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

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->addGuesser(new NameGuesser())
->build();
```
:::note
The guessers are queried in order of their priority, and the first match wins.
The built-in guesser is registered with priority `-64`, so your own guessers run
before the fallback to the `ObjectNormalizer`.
The built-in guesser is always asked last, so your own guessers run before the
fallback to the `ObjectNormalizer`.
:::

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

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Guesser\MappedGuesser;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->addGuesser(new MappedGuesser([
Name::class => NameNormalizer::class,
Email::class => EmailNormalizer::class,
Expand Down
11 changes: 4 additions & 7 deletions docs/hydrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,18 @@ 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 recommended way is the `StackHydratorBuilder`. It maps the properties with
reflection and comes with the `BuiltInGuesser`, which picks normalizers for
enums, date types and nested objects.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->build();
```
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 the same as a hydrator built by the builder:

```php
use Patchlevel\Hydrator\StackHydrator;
Expand Down
2 changes: 0 additions & 2 deletions docs/lazy.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ Instead of marking every class, you can make lazy hydration the default when
building the hydrator.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->enableDefaultLazy()
->build();
```
Expand Down
2 changes: 0 additions & 2 deletions docs/lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ result. For this, the `LifecycleExtension` provides four method attributes:
Register the `LifecycleExtension` on the builder:

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new LifecycleExtension())
->build();
```
Expand Down
2 changes: 0 additions & 2 deletions docs/upcasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ Register the `UpcastExtension` on the builder and pass it a list of upcasters.
Each upcaster receives the raw data array and returns a reshaped array.

```php
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;

$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new UpcastExtension(
beforeTransform: [
CallbackUpcaster::forClass(
Expand Down
10 changes: 5 additions & 5 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 @@ -127,9 +127,9 @@ parameters:
path: src/StackHydrator.php

-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Next\:\:hydrate\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\StackHydrator\:\:hydrateWithMiddlewares\(\) expects array\<string, mixed\>, array\<mixed, mixed\> given\.$#'
identifier: argument.type
count: 3
count: 2
path: src/StackHydrator.php

-
Expand Down
Loading
Loading