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
12 changes: 8 additions & 4 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,14 @@ asked last, so every class without its own transformer is transformed with
reflection.

The `TransformerResolver` gives the transformer insight into the hydrator which
created it. With `direct()` it asks whether the hydrator would call the
transformer of a nested class directly, without a middleware, a class
normalizer or a lazy proxy. Only then a transformer may map nested objects in
place, like the [generated transformers](generated-code.md) do.
created it. With `hydrateHandler()` and `extractHandler()` it gets what the
hydrator does for a nested class, and can call it directly instead of going
through the normalizer and the hydrator again. With `direct()` it asks whether
the hydrator would call the transformer of a nested class directly, without a
middleware, a class normalizer or a lazy proxy. Only then a transformer may map
nested objects in place, like the [generated transformers](generated-code.md)
do. Use the handlers only for calls of `hydrator()`: a hydrator which wraps it
has to see the nested objects.
:::warning
Use the resolver once the transformer is used, not inside `create()`. The
hydrator caches the transformer only after `create()` returned.
Expand Down
27 changes: 27 additions & 0 deletions docs/generated-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,33 @@ place for its calls, so it sees every nested object, see
[normalizers](normalizer.md).
:::

## Circular references

Like the reflection based mapping, the generated code throws a
`CircularReference` exception if an object leads back to itself while it is
extracted. To detect this, it keeps track of the objects it is currently
extracting. This is only needed for classes whose normalizers may call the
hydrator again: every normalizer except the built-in ones for dates, enums and
inline closures, since any normalizer can take the hydrator from the context.

If your objects can not be circular, you can disable the check to save this
work while extracting.

```php
use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension;

$extension = new GeneratedTransformerExtension(
__DIR__ . '/var/cache/hydrator',
circularReferenceCheck: false,
);
```
:::warning
Without the check, a circular reference is not detected. The extraction recurses
until PHP stops it with "Maximum call stack size reached", on PHP 8.2 the
process runs out of memory instead. Keep the check enabled in your tests to find
circular references there.
:::

## Learn more

* [How to build the hydrator](hydrator.md)
Expand Down
14 changes: 4 additions & 10 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,17 @@ parameters:
count: 1
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
path: src/StackHydrator.php

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

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

-
message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Extension\\Cryptography\\Fixture\\ChildWithSensitiveDataWithIdentifierDto\:\:\$email is never read, only written\.$#'
Expand Down
30 changes: 6 additions & 24 deletions src/Extension/Generated/ClassFingerprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
namespace Patchlevel\Hydrator\Extension\Generated;

use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
use Patchlevel\Hydrator\Normalizer\InvalidType;
use Patchlevel\Hydrator\Metadata\NestedObject;
use Patchlevel\Hydrator\Normalizer\Normalizer;
use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;
use ReflectionClass;

use function class_exists;
use function hash;
use function serialize;
use function substr;
Expand Down Expand Up @@ -73,32 +70,17 @@ public static function of(ClassMetadata $metadata): string
*/
public static function nested(Normalizer $normalizer): array|null
{
$array = false;
$nested = NestedObject::of($normalizer);

if ($normalizer instanceof ArrayNormalizer) {
$array = true;
$normalizer = $normalizer->innerNormalizer();
}

if (!$normalizer instanceof ObjectNormalizer) {
return null;
}

try {
$class = $normalizer->className();
} catch (InvalidType) {
return null;
}

if (!class_exists($class)) {
if ($nested === null) {
return null;
}

return [
'class' => $class,
'array' => $array,
'class' => $nested->className,
'array' => $nested->list,
// the generated code checks the exact class of nested objects, which is cheaper for final classes
'final' => (new ReflectionClass($class))->isFinal(),
'final' => (new ReflectionClass($nested->className))->isFinal(),
];
}
}
76 changes: 48 additions & 28 deletions src/Extension/Generated/CodeEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function emit(string $namespace, string $className): string
{
$plan = $this->plan;
$initialize = [];
$nestedInits = [];

foreach ($plan->properties as $property) {
if ($property->defaultSlot !== null) {
Expand All @@ -61,24 +62,26 @@ public function emit(string $namespace, string $className): string
$flag = $property->flag;
$nested = $property->nested;

$this->properties[] = sprintf('public ClassTransformer|HydrateHandler|null $hh%d = null;', $flag);
$this->properties[] = sprintf('public ClassTransformer|ExtractHandler|null $he%d = null;', $flag);
$this->properties[] = sprintf('public bool $ih%d = false;', $flag);
$this->properties[] = sprintf('public bool $ie%d = false;', $flag);
$initialize[] = sprintf('$h%d = $this->nested($this->n%d, \\%s::class, Direction::Hydrate);', $flag, $property->slot, $nested->class);
$initialize[] = sprintf('$e%d = $this->nested($this->n%d, \\%s::class, Direction::Extract);', $flag, $property->slot, $nested->class);
$initialize[] = sprintf('$this->ih%1$d = $h%1$d !== null;', $flag);
$initialize[] = sprintf('$this->ie%1$d = $e%1$d !== null;', $flag);
$initialize[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->x%2$d = $h%1$d ?? $e%1$d; }', $flag, $nested->slot);
$initialize[] = sprintf('$this->hh%d = $this->handler($this->n%d, \\%s::class, Direction::Hydrate);', $flag, $property->slot, $nested->class);
$initialize[] = sprintf('$this->he%d = $this->handler($this->n%d, \\%s::class, Direction::Extract);', $flag, $property->slot, $nested->class);
// generated transformers of nested classes are mapped in place, all others are called through their handler
$initialize[] = sprintf('$this->ih%1$d = $this->hh%1$d instanceof GeneratedTransformer;', $flag);
$initialize[] = sprintf('$this->ie%1$d = $this->he%1$d instanceof GeneratedTransformer;', $flag);
$this->properties[] = sprintf('public Closure|null $nh%d = null;', $flag);
$this->properties[] = sprintf('public Closure|null $ne%d = null;', $flag);
$nestedInits[] = sprintf('if ($this->ih%1$d) { $this->hh%1$d->init(); $this->nh%1$d = $this->hh%1$d->nestedHydrator; }', $flag);
$nestedInits[] = sprintf('if ($this->ie%1$d) { $this->he%1$d->init(); $this->ne%1$d = $this->he%1$d->nestedExtractor; }', $flag);

if ($property->inner === null) {
continue;
}

$this->properties[] = sprintf('public ObjectNormalizer $n%d;', $property->inner);
$initialize[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $flag, $property->inner, $property->slot);
}

foreach ($plan->nested() as $slot => $nested) {
$this->properties[] = sprintf('public GeneratedTransformer $x%d;', $slot);
$initialize[] = sprintf('if ($this->hh%1$d !== null || $this->he%1$d !== null) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $flag, $property->inner, $property->slot);
}

[$hydrate, $hydrateHelpers] = $this->hydrateBody();
Expand All @@ -88,6 +91,9 @@ public function emit(string $namespace, string $className): string
$initialize[] = $helper;
}

// the nested entry points: called directly by the transformers of other classes, without the entry checks
$nestedHydrate = "\$object = \$this->reflection->newInstanceWithoutConstructor();\n\n" . self::withInline($hydrate, 'true');
$nestedExtract = self::withInline($extract, 'true');
if ($plan->scopedHydrate) {
$this->properties[] = 'public Closure $h;';
$initialize[] = Templates::render(Templates::CLOSURE, [
Expand All @@ -112,9 +118,36 @@ public function emit(string $namespace, string $className): string
$extract = 'return ($this->e)($object, $context, $inline);';
}

foreach ($plan->nested() as $slot => $nested) {
// the nested transformers are initialized last, they may lead back to this one
$initialize[] = sprintf('if (isset($this->x%1$d)) { $this->x%1$d->init(); }', $slot);
if ($plan->scopedHydrate) {
$initialize[] = Templates::render(Templates::CLOSURE, [
'slot' => 'nestedHydrator',
'signature' => 'array $data, array $context',
'return' => 'object',
'body' => $nestedHydrate,
'class' => $plan->className(),
]);
$nestedHydrate = 'return ($this->nestedHydrator)($data, $context);';
}

if ($plan->scopedExtract) {
$initialize[] = Templates::render(Templates::CLOSURE, [
'slot' => 'nestedExtractor',
'signature' => 'object $object, array $context',
'return' => 'array',
'body' => $nestedExtract,
'class' => $plan->className(),
]);
$nestedExtract = 'return ($this->nestedExtractor)($object, $context);';
}

$nestedMethods = [
Templates::render(Templates::NESTED_HYDRATE, ['body' => $nestedHydrate]),
Templates::render(Templates::NESTED_EXTRACT, ['body' => $nestedExtract]),
];

// the nested transformers are initialized last, they may lead back to this one
foreach ($nestedInits as $init) {
$initialize[] = $init;
}

$code = Templates::render(Templates::TRANSFORMER, [
Expand All @@ -127,8 +160,7 @@ public function emit(string $namespace, string $className): string
// points are only called if they can
'hydrate' => self::withInline($hydrate, $plan->nested() === [] ? 'false' : Templates::OWNER),
'extract' => self::withInline($extract, $plan->nested() === [] ? 'false' : Templates::OWNER),
'hydrateNested' => self::withInline($hydrate, 'true'),
'extractNested' => self::withInline($extract, 'true'),
'nestedMethods' => implode("\n\n", $nestedMethods),
'initialize' => $initialize === [] ? '// nothing to resolve' : implode("\n", $initialize),
'recursive' => $this->recursive(),
]);
Expand Down Expand Up @@ -170,9 +202,8 @@ private function recursive(): string
}

$checks[] = sprintf(
'($this->ie%d ? $this->x%d->recursive() : self::mayRecurse($this->n%d))',
'($this->ie%1$d ? $this->he%1$d->recursive() : self::mayRecurse($this->n%2$d))',
$property->flag,
$property->nested->slot,
$property->slot,
);
}
Expand Down Expand Up @@ -248,15 +279,13 @@ private function denormalize(PropertyPlan $property, string $variable): string
'field' => $field,
'flag' => (string)$property->flag,
'slot' => (string)$property->slot,
'nested' => $this->nestedCall($property, 'hydrateNested'),
]),
ValueKind::NestedArray => Templates::render(Templates::DENORMALIZE_ARRAY, [
'variable' => $variable,
'field' => $field,
'flag' => (string)$property->flag,
'slot' => (string)$property->slot,
'inner' => (string)$property->inner,
'nested' => $this->nestedCall($property, 'hydrateNested'),
]),
};

Expand Down Expand Up @@ -362,7 +391,6 @@ private function normalize(PropertyPlan $property): array
'name' => $name,
'flag' => (string)$property->flag,
'slot' => (string)$property->slot,
'nested' => $this->nestedCall($property, 'extractNested'),
'check' => self::instanceCheck('$value', $property),
]),
ValueKind::NestedArray => Templates::render(Templates::NORMALIZE_ARRAY, [
Expand All @@ -371,7 +399,6 @@ private function normalize(PropertyPlan $property): array
'flag' => (string)$property->flag,
'slot' => (string)$property->slot,
'inner' => (string)$property->inner,
'nested' => $this->nestedCall($property, 'extractNested'),
'check' => self::instanceCheck('$item', $property),
]),
};
Expand All @@ -398,11 +425,4 @@ private static function instanceCheck(string $variable, PropertyPlan $property):

return sprintf('%1$s instanceof \\%2$s && %1$s::class === \\%2$s::class', $variable, $nested->class);
}

private function nestedCall(PropertyPlan $property, string $method): string
{
assert($property->nested !== null);

return sprintf('$this->x%d->%s', $property->nested->slot, $method);
}
}
Loading
Loading