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
162 changes: 162 additions & 0 deletions docs/UPGRADE-3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
searchable: false
---
# Upgrade 3.0

## Normalizer

### HydratorAwareNormalizer

`HydratorAwareNormalizer` has been removed. The hydrator is no longer injected
into the normalizers of the metadata, it is passed with every call in the
context under the key `Hydrator::HYDRATOR`. The `StackHydrator` adds itself to
the context, unless another hydrator which wraps it is already there. Remove
`setHydrator()` from your normalizers and read the hydrator from the context.

before:

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer;
use Patchlevel\Hydrator\Normalizer\MissingHydrator;
use Patchlevel\Hydrator\Normalizer\Normalizer;

final class ProfileNormalizer implements Normalizer, HydratorAwareNormalizer
{
private Hydrator|null $hydrator = null;

public function setHydrator(Hydrator $hydrator): void
{
$this->hydrator = $hydrator;
}

public function normalize(mixed $value, array $context): mixed
{
if (!$this->hydrator) {
throw new MissingHydrator();
}

return $this->hydrator->extract($value, $context);
}

public function denormalize(mixed $value, array $context): mixed
{
if (!$this->hydrator) {
throw new MissingHydrator();
}

return $this->hydrator->hydrate(Profile::class, $value, $context);
}
}
```
after:

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\Normalizer\MissingHydrator;
use Patchlevel\Hydrator\Normalizer\Normalizer;

final class ProfileNormalizer implements Normalizer
{
public function normalize(mixed $value, array $context): mixed
{
return $this->hydrator($context)->extract($value, $context);
}

public function denormalize(mixed $value, array $context): mixed
{
return $this->hydrator($context)->hydrate(Profile::class, $value, $context);
}

private function hydrator(array $context): Hydrator
{
$hydrator = $context[Hydrator::HYDRATOR] ?? null;

if (!$hydrator instanceof Hydrator) {
throw new MissingHydrator();
}

return $hydrator;
}
}
```
:::warning
Pass the context on when you call the hydrator from a normalizer, otherwise
nested objects lose the hydrator and all other context values.
:::

### Built-in normalizers

`ObjectNormalizer`, `ObjectMapNormalizer`, `ArrayNormalizer` and
`ArrayShapeNormalizer` no longer have a `setHydrator()` method. If you use them
outside of a hydrator, for example in a test, pass the hydrator in the context
instead.

before:

```php
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;

$normalizer = new ObjectNormalizer(Profile::class);
$normalizer->setHydrator($hydrator);

$data = $normalizer->normalize($profile, []);
```
after:

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;

$normalizer = new ObjectNormalizer(Profile::class);

$data = $normalizer->normalize($profile, [Hydrator::HYDRATOR => $hydrator]);
```
`ObjectNormalizer` and `ObjectMapNormalizer` throw a `MissingHydrator`
exception only if they actually need the hydrator. A `null` value is now
normalized and denormalized without one.

## Hydrator

### Context

The context which middlewares, normalizers, upcasters and lifecycle hooks
receive now contains the hydrator under the key `Hydrator::HYDRATOR`. Don't use
the key `hydrator` for your own context values. If you serialize, log or compare
the context, leave this key out.

### Wrapping a hydrator

If you wrap the hydrator in your own `Hydrator` implementation, add it to the
context before you call the inner hydrator. Nested objects then also go
through your wrapper.

```php
use Patchlevel\Hydrator\Hydrator;

final class LoggingHydrator implements Hydrator
{
public function __construct(
private readonly Hydrator $hydrator,
) {
}

public function hydrate(string $class, mixed $data, array $context = []): object
{
$context[Hydrator::HYDRATOR] ??= $this;

return $this->hydrator->hydrate($class, $data, $context);
}

public function extract(object $object, array $context = []): mixed
{
$context[Hydrator::HYDRATOR] ??= $this;

return $this->hydrator->extract($object, $context);
}
}
```
## Caching

The serialized form of the normalizers changed. Clear the
[metadata cache](caching.md) when you deploy the new version.
29 changes: 29 additions & 0 deletions docs/normalizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,35 @@ final class Profile
public Name $name;
}
```
### Nested objects

If your normalizer has to hydrate or extract another object, take the hydrator
from the context. The hydrator puts itself there under the key
`Hydrator::HYDRATOR` before it calls any normalizer. Pass the context on, so the
nested object gets the same hydrator and context values.

```php
use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\Normalizer\MissingHydrator;
use Patchlevel\Hydrator\Normalizer\Normalizer;

final class AddressNormalizer implements Normalizer
{
public function normalize(mixed $value, array $context): mixed
{
$hydrator = $context[Hydrator::HYDRATOR] ?? throw new MissingHydrator();

return $hydrator->extract($value, $context);
}

public function denormalize(mixed $value, array $context): mixed
{
$hydrator = $context[Hydrator::HYDRATOR] ?? throw new MissingHydrator();

return $hydrator->hydrate(Address::class, $value, $context);
}
}
```
## Define a normalizer on class level

Instead of specifying the normalizer on each property, you can also set the
Expand Down
1 change: 1 addition & 0 deletions docs/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{
"title": "Miscellaneous",
"subEntries": [
{ "title": "Upgrade from 2.x", "file": "UPGRADE-3.0.md" },
{ "title": "Upgrade from 1.x", "file": "UPGRADE-2.0md" }
]
}
Expand Down
6 changes: 6 additions & 0 deletions src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ interface Hydrator
{
public const OBJECT_TO_POPULATE = 'object_to_populate';

/**
* The hydrator which started the call, normalizers use it to hydrate and extract nested objects. It is set by the
* outermost hydrator, so a hydrator which wraps another one also sees the nested objects.
*/
public const HYDRATOR = 'hydrator';

/**
* @param class-string<T> $class
* @param array<string, mixed> $context
Expand Down
12 changes: 1 addition & 11 deletions src/Normalizer/ArrayNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
namespace Patchlevel\Hydrator\Normalizer;

use Attribute;
use Patchlevel\Hydrator\Hydrator;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\NullableType;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class ArrayNormalizer implements Normalizer, TypeAwareNormalizer, HydratorAwareNormalizer
final readonly class ArrayNormalizer implements Normalizer, TypeAwareNormalizer
{
public function __construct(
private Normalizer $normalizer,
Expand Down Expand Up @@ -68,15 +67,6 @@ public function denormalize(mixed $value, array $context): array|null
return $result;
}

public function setHydrator(Hydrator $hydrator): void
{
if (!$this->normalizer instanceof HydratorAwareNormalizer) {
return;
}

$this->normalizer->setHydrator($hydrator);
}

public function handleType(Type|null $type): void
{
if ($type === null) {
Expand Down
14 changes: 1 addition & 13 deletions src/Normalizer/ArrayShapeNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
namespace Patchlevel\Hydrator\Normalizer;

use Attribute;
use Patchlevel\Hydrator\Hydrator;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\ArrayShapeType;
use Symfony\Component\TypeInfo\Type\NullableType;

use function is_array;

#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class ArrayShapeNormalizer implements Normalizer, TypeAwareNormalizer, HydratorAwareNormalizer
final readonly class ArrayShapeNormalizer implements Normalizer, TypeAwareNormalizer
{
/** @param array<array-key, Normalizer> $normalizerMap */
public function __construct(
Expand Down Expand Up @@ -77,17 +76,6 @@ public function denormalize(mixed $value, array $context): array|null
return $result;
}

public function setHydrator(Hydrator $hydrator): void
{
foreach ($this->normalizerMap as $normalizer) {
if (!$normalizer instanceof HydratorAwareNormalizer) {
continue;
}

$normalizer->setHydrator($hydrator);
}
}

public function handleType(Type|null $type): void
{
if ($type === null) {
Expand Down
12 changes: 0 additions & 12 deletions src/Normalizer/HydratorAwareNormalizer.php

This file was deleted.

5 changes: 4 additions & 1 deletion src/Normalizer/MissingHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace Patchlevel\Hydrator\Normalizer;

use Patchlevel\Hydrator\Hydrator;
use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

use function sprintf;

final class MissingHydrator extends RuntimeException implements HydratorException
{
public function __construct()
{
parent::__construct('no hydrator set');
parent::__construct(sprintf('no hydrator in the context, it is expected under the key "%s"', Hydrator::HYDRATOR));
}
}
Loading
Loading