diff --git a/docs/extensions.md b/docs/extensions.md index e91954ce..c942b00a 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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. diff --git a/docs/generated-code.md b/docs/generated-code.md index 3118a528..210728aa 100644 --- a/docs/generated-code.md +++ b/docs/generated-code.md @@ -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) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e5e89c19..20a3502e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -120,23 +120,17 @@ parameters: count: 1 path: src/Normalizer/ObjectNormalizer.php - - - message: '#^Method Patchlevel\\Hydrator\\StackHydrator\:\:middlewaresFor\(\) should return non\-empty\-list\ but returns list\\.$#' - identifier: return.type - count: 1 - path: src/StackHydrator.php - - message: '#^Parameter \#1 \$data of method Patchlevel\\Hydrator\\Transformer\\ClassTransformer\:\:hydrate\(\) expects array\, array\ given\.$#' identifier: argument.type - count: 2 + count: 1 path: src/StackHydrator.php - - message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\StackHydrator\:\:hydrateWithMiddlewares\(\) expects array\, array\ given\.$#' + message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Middleware\\Next\:\:hydrate\(\) expects array\, array\ 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\.$#' diff --git a/src/Extension/Generated/ClassFingerprint.php b/src/Extension/Generated/ClassFingerprint.php index 548f76c1..631ee7c6 100644 --- a/src/Extension/Generated/ClassFingerprint.php +++ b/src/Extension/Generated/ClassFingerprint.php @@ -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; @@ -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(), ]; } } diff --git a/src/Extension/Generated/CodeEmitter.php b/src/Extension/Generated/CodeEmitter.php index 07d436d6..fb6c829a 100644 --- a/src/Extension/Generated/CodeEmitter.php +++ b/src/Extension/Generated/CodeEmitter.php @@ -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) { @@ -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(); @@ -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, [ @@ -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, [ @@ -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(), ]); @@ -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, ); } @@ -248,7 +279,6 @@ 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, @@ -256,7 +286,6 @@ private function denormalize(PropertyPlan $property, string $variable): string 'flag' => (string)$property->flag, 'slot' => (string)$property->slot, 'inner' => (string)$property->inner, - 'nested' => $this->nestedCall($property, 'hydrateNested'), ]), }; @@ -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, [ @@ -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), ]), }; @@ -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); - } } diff --git a/src/Extension/Generated/GeneratedTransformer.php b/src/Extension/Generated/GeneratedTransformer.php index 2555b7b7..5d4c9b9b 100644 --- a/src/Extension/Generated/GeneratedTransformer.php +++ b/src/Extension/Generated/GeneratedTransformer.php @@ -4,12 +4,20 @@ namespace Patchlevel\Hydrator\Extension\Generated; +use Closure; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; use Patchlevel\Hydrator\Normalizer\ArrayShapeNormalizer; +use Patchlevel\Hydrator\Normalizer\DateIntervalNormalizer; +use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; +use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer; +use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer; +use Patchlevel\Hydrator\Normalizer\EnumNormalizer; +use Patchlevel\Hydrator\Normalizer\InlineNormalizer; use Patchlevel\Hydrator\Normalizer\Normalizer; -use Patchlevel\Hydrator\Normalizer\ObjectMapNormalizer; use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; use Patchlevel\Hydrator\Transformer\CallStack; use Patchlevel\Hydrator\Transformer\ClassTransformer; @@ -22,9 +30,9 @@ /** * Base of the generated transformers. * - * The transformers are initialized on the first use: only then the hydrator knows whether it passes the nested - * classes to generated transformers directly. Only then nested objects are mapped in place, with the nested entry - * points of the transformer of the nested class. + * The transformers are initialized on the first use: only then the hydrator knows what it does for the nested + * classes. Nested objects are mapped with the handlers of their classes directly, and in place with the nested entry + * points if the handler is a generated transformer. * * The members are public, because the generated code also runs in closures bound to the scope of the transformed * class to access its private properties. @@ -46,14 +54,33 @@ abstract class GeneratedTransformer implements ClassTransformer /** @var ReflectionClass */ public ReflectionClass $reflection; - /** @param ClassMetadata $metadata */ + /** + * @param ClassMetadata $metadata + * @param bool $circularReferenceCheck track extracted objects to detect circular references + */ public function __construct( public readonly ClassMetadata $metadata, public readonly TransformerResolver $resolver, public readonly CallStack $callStack, + public readonly bool $circularReferenceCheck = true, ) { } + /** + * The code of classes with private properties runs in a closure bound to their scope. Other transformers call it + * directly for nested objects, instead of {@see self::hydrateNested()} which only forwards to it. + * + * @var (Closure(array, array): object)|null + */ + public Closure|null $nestedHydrator = null; + + /** + * See {@see self::$nestedHydrator}. + * + * @var (Closure(object, array): array)|null + */ + public Closure|null $nestedExtractor = null; + /** * Hydrates a nested object in place. Called by an initialized transformer for calls from the owner, without an * object to populate. @@ -86,7 +113,7 @@ final public function init(): void $this->owner = $this->resolver->hydrator(); $this->reflection = $this->metadata->reflection; $this->initialize(); - $this->tracked = $this->recursive(); + $this->tracked = $this->circularReferenceCheck && $this->recursive(); $this->ready = true; } finally { $this->initializing = false; @@ -124,12 +151,12 @@ final protected function promotedDefault(string $property): ReflectionParameter } /** - * The generated transformer of a nested class, if nested objects can be mapped in place: the hydrator must call - * the generated transformer of the nested class directly, without a middleware, a class normalizer or a lazy proxy. + * The handler of a nested class, which maps its objects like the hydrator does, without the detour through the + * normalizer and the hydrator. A generated transformer is mapped in place with its nested entry points. * * @param class-string $class */ - final protected function nested(Normalizer $normalizer, string $class, Direction $direction): GeneratedTransformer|null + final protected function handler(Normalizer $normalizer, string $class, Direction $direction): ClassTransformer|HydrateHandler|ExtractHandler|null { if ($normalizer instanceof ArrayNormalizer) { $normalizer = $normalizer->innerNormalizer(); @@ -144,16 +171,20 @@ final protected function nested(Normalizer $normalizer, string $class, Direction return null; } - $transformer = $this->resolver->direct($class, $direction); + // initialized by the caller once all its normalizers are resolved, the nested class may lead back to it + return $direction === Direction::Hydrate + ? $this->resolver->hydrateHandler($class) + : $this->resolver->extractHandler($class); } catch (Throwable) { return null; } - - // initialized by the caller once all its normalizers are resolved, the nested class may lead back to it - return $transformer instanceof self ? $transformer : null; } - /** Whether the normalizer extracts objects with the hydrator, which can lead back to the same object. */ + /** + * Whether the normalizer may extract objects with the hydrator, which can lead back to the same object. Only the + * built-in normalizers which never call the hydrator are ruled out, every other normalizer could take it from the + * context. + */ final protected static function mayRecurse(Normalizer $normalizer): bool { if ($normalizer instanceof ArrayNormalizer) { @@ -170,6 +201,11 @@ final protected static function mayRecurse(Normalizer $normalizer): bool return false; } - return $normalizer instanceof ObjectNormalizer || $normalizer instanceof ObjectMapNormalizer; + return !$normalizer instanceof DateTimeImmutableNormalizer + && !$normalizer instanceof DateTimeNormalizer + && !$normalizer instanceof DateTimeZoneNormalizer + && !$normalizer instanceof DateIntervalNormalizer + && !$normalizer instanceof EnumNormalizer + && !$normalizer instanceof InlineNormalizer; } } diff --git a/src/Extension/Generated/GeneratedTransformerExtension.php b/src/Extension/Generated/GeneratedTransformerExtension.php index cfe7ce7b..9496d35c 100644 --- a/src/Extension/Generated/GeneratedTransformerExtension.php +++ b/src/Extension/Generated/GeneratedTransformerExtension.php @@ -13,14 +13,22 @@ */ final class GeneratedTransformerExtension implements Extension { - /** @param string $cachePath Directory where the {@see GeneratedTransformerWarmer} stored the generated code. */ + /** + * @param string $cachePath Directory where the {@see GeneratedTransformerWarmer} stored the generated code. + * @param bool $circularReferenceCheck Track extracted objects to detect circular references. Without it, the + * generated code is faster, but a circular reference ends in PHP's stack limit. + */ public function __construct( private readonly string $cachePath, + private readonly bool $circularReferenceCheck = true, ) { } public function configure(StackHydratorBuilder $builder): void { - $builder->addTransformerFactory(new GeneratedTransformerFactory(new TransformerFiles($this->cachePath))); + $builder->addTransformerFactory(new GeneratedTransformerFactory( + new TransformerFiles($this->cachePath), + $this->circularReferenceCheck, + )); } } diff --git a/src/Extension/Generated/GeneratedTransformerFactory.php b/src/Extension/Generated/GeneratedTransformerFactory.php index 00f8f673..5f04fcd3 100644 --- a/src/Extension/Generated/GeneratedTransformerFactory.php +++ b/src/Extension/Generated/GeneratedTransformerFactory.php @@ -27,8 +27,10 @@ final class GeneratedTransformerFactory implements ClassTransformerFactory /** @var WeakMap the generated transformers of a hydrator share their call stack */ private WeakMap $callStacks; + /** @param bool $circularReferenceCheck track extracted objects to detect circular references */ public function __construct( private readonly TransformerFiles $files, + private readonly bool $circularReferenceCheck = true, ) { $this->callStacks = new WeakMap(); } @@ -48,7 +50,12 @@ public function create(ClassMetadata $metadata, TransformerResolver $resolver): require_once $file; } - $transformer = new $fqcn($metadata, $resolver, $this->callStacks[$resolver] ??= new CallStack()); + $transformer = new $fqcn( + $metadata, + $resolver, + $this->callStacks[$resolver] ??= new CallStack(), + $this->circularReferenceCheck, + ); assert($transformer instanceof GeneratedTransformer); return $transformer; diff --git a/src/Extension/Generated/Templates.php b/src/Extension/Generated/Templates.php index 9a992473..11c4fa72 100644 --- a/src/Extension/Generated/Templates.php +++ b/src/Extension/Generated/Templates.php @@ -70,10 +70,13 @@ static function (array $matches) use ($value): string { use Patchlevel\Hydrator\CircularReference; use Patchlevel\Hydrator\DenormalizationFailure; use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformer; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\NormalizationFailure; use Patchlevel\Hydrator\Normalizer\Normalizer; use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; +use Patchlevel\Hydrator\Transformer\ClassTransformer; use Patchlevel\Hydrator\Transformer\Direction; use Patchlevel\Hydrator\TypeMismatch; use ReflectionParameter; @@ -114,17 +117,7 @@ public function extract(object $object, array $context): array {{extract}} } - public function hydrateNested(array $data, array $context): object - { - $object = $this->reflection->newInstanceWithoutConstructor(); - - {{hydrateNested}} - } - - public function extractNested(object $object, array $context): array - { - {{extractNested}} - } + {{nestedMethods}} protected function initialize(): void { @@ -137,6 +130,20 @@ protected function mayReachItself(): bool } } +PHP; + + public const NESTED_HYDRATE = <<<'PHP' +public function hydrateNested(array $data, array $context): object +{ + {{body}} +} +PHP; + + public const NESTED_EXTRACT = <<<'PHP' +public function extractNested(object $object, array $context): array +{ + {{body}} +} PHP; public const CLOSURE = <<<'PHP' @@ -231,7 +238,9 @@ protected function mayReachItself(): bool {{variable}} = $data[{{field}}]; if ($this->ih{{flag}} && $inline && \is_array({{variable}})) { - {{variable}} = {{nested}}({{variable}}, $context); + {{variable}} = $this->nh{{flag}} !== null ? ($this->nh{{flag}})({{variable}}, $context) : $this->hh{{flag}}->hydrateNested({{variable}}, $context); +} elseif ($this->hh{{flag}} !== null && $inline && \is_array({{variable}})) { + {{variable}} = $this->hh{{flag}}->hydrate({{variable}}, $context); } else { {{variable}} = $this->n{{slot}}->denormalize({{variable}}, $context); } @@ -243,8 +252,22 @@ protected function mayReachItself(): bool if ($this->ih{{flag}} && $inline && \is_array({{variable}})) { $items = []; + if ($this->nh{{flag}} !== null) { + foreach ({{variable}} as $key => $item) { + $items[$key] = \is_array($item) ? ($this->nh{{flag}})($item, $context) : $this->n{{inner}}->denormalize($item, $context); + } + } else { + foreach ({{variable}} as $key => $item) { + $items[$key] = \is_array($item) ? $this->hh{{flag}}->hydrateNested($item, $context) : $this->n{{inner}}->denormalize($item, $context); + } + } + + {{variable}} = $items; +} elseif ($this->hh{{flag}} !== null && $inline && \is_array({{variable}})) { + $items = []; + foreach ({{variable}} as $key => $item) { - $items[$key] = \is_array($item) ? {{nested}}($item, $context) : $this->n{{inner}}->denormalize($item, $context); + $items[$key] = \is_array($item) ? $this->hh{{flag}}->hydrate($item, $context) : $this->n{{inner}}->denormalize($item, $context); } {{variable}} = $items; @@ -265,7 +288,14 @@ protected function mayReachItself(): bool public const NORMALIZE_OBJECT = <<<'PHP' $value = $object->{{name}}; -{{variable}} = $this->ie{{flag}} && $inline && {{check}} ? {{nested}}($value, $context) : $this->n{{slot}}->normalize($value, $context); + +if ($this->ie{{flag}} && $inline && {{check}}) { + {{variable}} = $this->ne{{flag}} !== null ? ($this->ne{{flag}})($value, $context) : $this->he{{flag}}->extractNested($value, $context); +} elseif ($this->he{{flag}} !== null && $inline && {{check}}) { + {{variable}} = $this->he{{flag}}->extract($value, $context); +} else { + {{variable}} = $this->n{{slot}}->normalize($value, $context); +} PHP; public const NORMALIZE_ARRAY = <<<'PHP' @@ -274,8 +304,22 @@ protected function mayReachItself(): bool if ($this->ie{{flag}} && $inline && \is_array($value)) { $items = []; + if ($this->ne{{flag}} !== null) { + foreach ($value as $key => $item) { + $items[$key] = {{check}} ? ($this->ne{{flag}})($item, $context) : $this->n{{inner}}->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = {{check}} ? $this->he{{flag}}->extractNested($item, $context) : $this->n{{inner}}->normalize($item, $context); + } + } + + {{variable}} = $items; +} elseif ($this->he{{flag}} !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = {{check}} ? {{nested}}($item, $context) : $this->n{{inner}}->normalize($item, $context); + $items[$key] = {{check}} ? $this->he{{flag}}->extract($item, $context) : $this->n{{inner}}->normalize($item, $context); } {{variable}} = $items; diff --git a/src/Extension/Generated/TransformerFiles.php b/src/Extension/Generated/TransformerFiles.php index 88fdd3e4..8ea7ddac 100644 --- a/src/Extension/Generated/TransformerFiles.php +++ b/src/Extension/Generated/TransformerFiles.php @@ -24,7 +24,7 @@ final class TransformerFiles public const NAMESPACE = 'Patchlevel\\Hydrator\\Generated'; /** Part of the name, bump it whenever the generated code changes. */ - public const VERSION = 10; + public const VERSION = 12; public function __construct( public readonly string $cachePath, diff --git a/src/Handler/ExtractHandler.php b/src/Handler/ExtractHandler.php new file mode 100644 index 00000000..3ec4db57 --- /dev/null +++ b/src/Handler/ExtractHandler.php @@ -0,0 +1,22 @@ + $context + * + * @throws HydratorException + */ + public function extract(object $object, array $context): mixed; +} diff --git a/src/Handler/HydrateHandler.php b/src/Handler/HydrateHandler.php new file mode 100644 index 00000000..fdeed8d2 --- /dev/null +++ b/src/Handler/HydrateHandler.php @@ -0,0 +1,22 @@ + $context + * + * @throws HydratorException + */ + public function hydrate(mixed $data, array $context): object; +} diff --git a/src/Handler/LazyHandler.php b/src/Handler/LazyHandler.php new file mode 100644 index 00000000..ac01455c --- /dev/null +++ b/src/Handler/LazyHandler.php @@ -0,0 +1,40 @@ + $reflection */ + public function __construct( + private readonly ReflectionClass $reflection, + private readonly ClassTransformer|HydrateHandler $handler, + ) { + } + + /** @param array $context */ + public function hydrate(mixed $data, array $context): object + { + if (!is_array($data)) { + throw new ArrayDataRequired($this->reflection->getName()); + } + + $handler = $this->handler; + + return $this->reflection->newLazyProxy( + static fn (): object => $handler->hydrate($data, $context), + ); + } +} diff --git a/src/Handler/MiddlewareHandler.php b/src/Handler/MiddlewareHandler.php new file mode 100644 index 00000000..6c9573ed --- /dev/null +++ b/src/Handler/MiddlewareHandler.php @@ -0,0 +1,46 @@ + $metadata */ + public function __construct( + private readonly ClassMetadata $metadata, + private readonly Next $next, + ) { + } + + /** @param array $context */ + public function hydrate(mixed $data, array $context): object + { + if (!is_array($data)) { + throw new ArrayDataRequired($this->metadata->className); + } + + return $this->next->hydrate($this->metadata, $data, $context); + } + + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context): array + { + return $this->next->extract($this->metadata, $object, $context); + } +} diff --git a/src/Handler/NormalizerHandler.php b/src/Handler/NormalizerHandler.php new file mode 100644 index 00000000..a7b380cc --- /dev/null +++ b/src/Handler/NormalizerHandler.php @@ -0,0 +1,41 @@ + $context */ + public function hydrate(mixed $data, array $context): object + { + $object = $this->normalizer->denormalize($data, $context); + + if (!$object instanceof $this->class) { + throw new ObjectRequired($this->class, $this->normalizer::class); + } + + return $object; + } + + /** @param array $context */ + public function extract(object $object, array $context): mixed + { + return $this->normalizer->normalize($object, $context); + } +} diff --git a/src/Metadata/NestedObject.php b/src/Metadata/NestedObject.php new file mode 100644 index 00000000..ba24c8fb --- /dev/null +++ b/src/Metadata/NestedObject.php @@ -0,0 +1,53 @@ +innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + $className = $normalizer->className(); + } catch (InvalidType) { + return null; + } + + if (!class_exists($className)) { + return null; + } + + return new self($className, $list); + } +} diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index 3ab336a3..2e736da1 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -33,6 +33,12 @@ public function __construct( $this->propertyName = $reflection->getName(); } + /** The nested object of the property, derived from its normalizer. */ + public function nested(): NestedObject|null + { + return $this->normalizer === null ? null : NestedObject::of($this->normalizer); + } + public function setValue(object $object, mixed $value): void { $this->reflection->setValue($object, $value); diff --git a/src/Middleware/Next.php b/src/Middleware/Next.php index da06ce87..a596a03c 100644 --- a/src/Middleware/Next.php +++ b/src/Middleware/Next.php @@ -7,21 +7,28 @@ use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Transformer\ClassTransformer; +use function array_slice; use function assert; /** * The rest of the middleware stack. A middleware calls it to pass the data on to the next middleware, after the last * one the transformer maps the data to the object and back. + * + * The stack is an immutable chain which is built once per class and reused for every call, so a middleware can call + * the rest of the stack more than once. */ final class Next { - private int $index = 0; + private readonly Middleware|null $middleware; + private readonly Next|null $next; /** @param list $middlewares */ public function __construct( - private readonly array $middlewares, + array $middlewares, private readonly ClassTransformer $transformer, ) { + $this->middleware = $middlewares[0] ?? null; + $this->next = $this->middleware === null ? null : new self(array_slice($middlewares, 1), $transformer); } /** @@ -35,23 +42,14 @@ public function __construct( */ public function hydrate(ClassMetadata $metadata, array $data, array $context): object { - $middleware = $this->middlewares[$this->index] ?? null; - - if ($middleware === null) { + if ($this->middleware === null || $this->next === null) { $object = $this->transformer->hydrate($data, $context); assert($object instanceof $metadata->className); return $object; } - $this->index++; - - try { - return $middleware->hydrate($metadata, $data, $context, $this); - } finally { - // the position is restored, so a middleware can call the rest of the stack more than once - $this->index--; - } + return $this->middleware->hydrate($metadata, $data, $context, $this->next); } /** @@ -65,18 +63,10 @@ public function hydrate(ClassMetadata $metadata, array $data, array $context): o */ public function extract(ClassMetadata $metadata, object $object, array $context): array { - $middleware = $this->middlewares[$this->index] ?? null; - - if ($middleware === null) { + if ($this->middleware === null || $this->next === null) { return $this->transformer->extract($object, $context); } - $this->index++; - - try { - return $middleware->extract($metadata, $object, $context, $this); - } finally { - $this->index--; - } + return $this->middleware->extract($metadata, $object, $context, $this->next); } } diff --git a/src/StackHydrator.php b/src/StackHydrator.php index f2957162..b7234cbe 100644 --- a/src/StackHydrator.php +++ b/src/StackHydrator.php @@ -4,6 +4,11 @@ namespace Patchlevel\Hydrator; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; +use Patchlevel\Hydrator\Handler\LazyHandler; +use Patchlevel\Hydrator\Handler\MiddlewareHandler; +use Patchlevel\Hydrator\Handler\NormalizerHandler; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Metadata\ClassNotFound; @@ -17,7 +22,6 @@ use Patchlevel\Hydrator\Transformer\Direction; use Patchlevel\Hydrator\Transformer\ReflectionTransformerFactory; use Patchlevel\Hydrator\Transformer\TransformerResolver; -use ReflectionClass; use function array_key_exists; use function assert; @@ -25,27 +29,24 @@ use const PHP_VERSION_ID; +/** + * Decides once per class and direction what has to happen and keeps it as a handler: the transformer itself if + * nothing else has to run, otherwise a handler which calls the class normalizer, the middlewares or creates a lazy + * proxy. Every call is then a lookup of the handler and a call of it. + */ final class StackHydrator implements Hydrator { /** @var array */ private array $classMetadata = []; - /** @var array> */ - private array $hydrateMiddlewares = []; - - /** @var array> */ - private array $extractMiddlewares = []; - /** @var array */ private array $transformers = []; - /** @var array false if the hydrator does more than calling the transformer */ - private array $directHydrate = []; - - /** @var array false if the hydrator does more than calling the transformer */ - private array $directExtract = []; + /** @var array */ + private array $hydrateHandlers = []; - private readonly bool $hasSkippableMiddlewares; + /** @var array */ + private array $extractHandlers = []; private readonly TransformerResolver $resolver; @@ -67,18 +68,11 @@ public function __construct( private readonly bool $defaultLazy = false, private readonly ClassTransformerFactory $transformerFactory = new ReflectionTransformerFactory(), ) { - $hasSkippableMiddlewares = false; - - foreach ($middlewares as $middleware) { - if ($middleware instanceof SkippableMiddleware) { - $hasSkippableMiddlewares = true; - - break; - } - } - - $this->hasSkippableMiddlewares = $hasSkippableMiddlewares; - $this->resolver = new StackTransformerResolver($this, $this->direct(...)); + $this->resolver = new StackTransformerResolver( + $this, + fn (string $class): ClassTransformer|HydrateHandler => $this->hydrateHandlers[$class] ?? $this->hydrateHandler($class), + fn (string $class): ClassTransformer|ExtractHandler => $this->extractHandlers[$class] ?? $this->extractHandler($class), + ); $this->context = [self::HYDRATOR => $this]; } @@ -98,69 +92,25 @@ public function hydrate(string $class, mixed $data, array $context = []): object $context[self::HYDRATOR] ??= $this; } - $transformer = $this->directHydrate[$class] ?? false; - - // nothing else to do for this class, the transformer is called right away - if ($transformer !== false && is_array($data)) { - $object = $transformer->hydrate($data, $context); - assert($object instanceof $class); - - return $object; - } - - return $this->hydrateClass($class, $data, $context); - } - - /** - * The path for classes seen the first time and for classes which need more than the transformer, kept out of - * {@see self::hydrate()} to keep the direct path small. - * - * @param class-string $class - * @param array $context - * - * @return T - * - * @template T of object - */ - private function hydrateClass(string $class, mixed $data, array $context): object - { try { - $transformer = $this->directHydrate[$class] ?? $this->direct($class, Direction::Hydrate); + $handler = $this->hydrateHandlers[$class] ?? $this->hydrateHandler($class); } catch (ClassNotFound $e) { throw new ClassNotSupported($class, $e); } - if ($transformer !== false && is_array($data)) { - $object = $transformer->hydrate($data, $context); - assert($object instanceof $class); - - return $object; - } - - /** @var ClassMetadata $metadata the metadata is already loaded */ - $metadata = $this->classMetadata[$class]; - - if ($metadata->normalizer) { - $return = $metadata->normalizer->denormalize($data, $context); - - if (!$return instanceof $class) { - throw new ObjectRequired($class, $metadata->normalizer::class); + if ($handler instanceof ClassTransformer) { + if (!is_array($data)) { + throw new ArrayDataRequired($class); } - return $return; - } - - if (!is_array($data)) { - throw new ArrayDataRequired($class); + $object = $handler->hydrate($data, $context); + } else { + $object = $handler->hydrate($data, $context); } - if (!$this->lazy($metadata)) { - return $this->hydrateWithMiddlewares($metadata, $data, $context); - } + assert($object instanceof $class); - return (new ReflectionClass($class))->newLazyProxy( - fn (): object => $this->hydrateWithMiddlewares($metadata, $data, $context), - ); + return $object; } /** @@ -176,97 +126,53 @@ public function extract(object $object, array $context = []): mixed $context[self::HYDRATOR] ??= $this; } - $transformer = $this->directExtract[$object::class] ?? false; - - // nothing else to do for this class, the transformer is called right away - if ($transformer !== false) { - return $transformer->extract($object, $context); - } + $handler = $this->extractHandlers[$object::class] ?? $this->extractHandler($object::class); - return $this->extractClass($object, $context); + return $handler->extract($object, $context); } /** - * The path for classes seen the first time and for classes which need more than the transformer, kept out of - * {@see self::extract()} to keep the direct path small. + * @param class-string $class * - * @param array $context + * @throws ClassNotFound */ - private function extractClass(object $object, array $context): mixed + private function hydrateHandler(string $class): ClassTransformer|HydrateHandler { - $transformer = $this->directExtract[$object::class] ?? $this->direct($object::class, Direction::Extract); + $metadata = $this->metadata($class); - if ($transformer !== false) { - return $transformer->extract($object, $context); + if ($metadata->normalizer !== null) { + return $this->hydrateHandlers[$class] = new NormalizerHandler($class, $metadata->normalizer); } - $metadata = $this->classMetadata[$object::class]; + $transformer = $this->transformer($metadata); + $middlewares = $this->middlewaresFor($metadata, Direction::Hydrate); + + $handler = $middlewares === [] + ? $transformer + : new MiddlewareHandler($metadata, new Next($middlewares, $transformer)); - if ($metadata->normalizer) { - return $metadata->normalizer->normalize($object, $context); + if (PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->defaultLazy)) { + $handler = new LazyHandler($metadata->reflection, $handler); } - return (new Next($this->middlewaresFor($metadata, Direction::Extract), $this->transformer($metadata))) - ->extract($metadata, $object, $context); + return $this->hydrateHandlers[$class] = $handler; } - /** - * Decided once per class and direction: the transformer can be called directly if the class has no class - * normalizer, is not lazy and no middleware runs for it. - * - * @param class-string $class - * - * @throws ClassNotFound - */ - private function direct(string $class, Direction $direction): ClassTransformer|false + /** @param class-string $class */ + private function extractHandler(string $class): ClassTransformer|ExtractHandler { $metadata = $this->metadata($class); - $direct = false; - - if ( - $metadata->normalizer === null - && ($direction === Direction::Extract || !$this->lazy($metadata)) - && $this->middlewaresFor($metadata, $direction) === [] - ) { - $direct = $this->transformer($metadata); - } - if ($direction === Direction::Hydrate) { - return $this->directHydrate[$class] = $direct; + if ($metadata->normalizer !== null) { + return $this->extractHandlers[$class] = new NormalizerHandler($class, $metadata->normalizer); } - return $this->directExtract[$class] = $direct; - } - - /** @param ClassMetadata $metadata */ - private function lazy(ClassMetadata $metadata): bool - { - return PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->defaultLazy); - } - - /** - * @param ClassMetadata $metadata - * @param array $data - * @param array $context - * - * @return T - * - * @template T of object - */ - private function hydrateWithMiddlewares(ClassMetadata $metadata, array $data, array $context): object - { - $middlewares = $this->middlewaresFor($metadata, Direction::Hydrate); $transformer = $this->transformer($metadata); + $middlewares = $this->middlewaresFor($metadata, Direction::Extract); - if ($middlewares === []) { - // no middleware runs for this class, the transformer is called without building the stack - $object = $transformer->hydrate($data, $context); - assert($object instanceof $metadata->className); - - return $object; - } - - return (new Next($middlewares, $transformer))->hydrate($metadata, $data, $context); + return $this->extractHandlers[$class] = $middlewares === [] + ? $transformer + : new MiddlewareHandler($metadata, new Next($middlewares, $transformer)); } /** @@ -290,20 +196,8 @@ private function transformer(ClassMetadata $metadata): ClassTransformer */ private function middlewaresFor(ClassMetadata $metadata, Direction $direction): array { - if (!$this->hasSkippableMiddlewares) { - return $this->middlewares; - } - - $cached = $direction === Direction::Hydrate - ? $this->hydrateMiddlewares[$metadata->className] ?? null - : $this->extractMiddlewares[$metadata->className] ?? null; - - if ($cached !== null) { - return $cached; - } - - $middlewares = []; $skipped = $direction === Direction::Hydrate ? Skip::Hydrate : Skip::Extract; + $middlewares = []; foreach ($this->middlewares as $middleware) { if ($middleware instanceof SkippableMiddleware) { @@ -317,11 +211,7 @@ private function middlewaresFor(ClassMetadata $metadata, Direction $direction): $middlewares[] = $middleware; } - if ($direction === Direction::Hydrate) { - return $this->hydrateMiddlewares[$metadata->className] = $middlewares; - } - - return $this->extractMiddlewares[$metadata->className] = $middlewares; + return $middlewares; } /** diff --git a/src/StackTransformerResolver.php b/src/StackTransformerResolver.php index e00d5e44..a771d02e 100644 --- a/src/StackTransformerResolver.php +++ b/src/StackTransformerResolver.php @@ -5,6 +5,8 @@ namespace Patchlevel\Hydrator; use Closure; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Transformer\ClassTransformer; use Patchlevel\Hydrator\Transformer\Direction; @@ -13,10 +15,14 @@ /** @internal the resolver the {@see StackHydrator} passes to its transformer factory */ final class StackTransformerResolver implements TransformerResolver { - /** @param Closure(class-string, Direction): (ClassTransformer|false) $direct */ + /** + * @param Closure(class-string): (ClassTransformer|HydrateHandler) $hydrateHandler + * @param Closure(class-string): (ClassTransformer|ExtractHandler) $extractHandler + */ public function __construct( private readonly StackHydrator $hydrator, - private readonly Closure $direct, + private readonly Closure $hydrateHandler, + private readonly Closure $extractHandler, ) { } @@ -37,9 +43,25 @@ public function metadata(string $class): ClassMetadata return $this->hydrator->metadata($class); } + /** @param class-string $class */ + public function hydrateHandler(string $class): ClassTransformer|HydrateHandler + { + return ($this->hydrateHandler)($class); + } + + /** @param class-string $class */ + public function extractHandler(string $class): ClassTransformer|ExtractHandler + { + return ($this->extractHandler)($class); + } + /** @param class-string $class */ public function direct(string $class, Direction $direction): ClassTransformer|null { - return ($this->direct)($class, $direction) ?: null; + $handler = $direction === Direction::Hydrate + ? ($this->hydrateHandler)($class) + : ($this->extractHandler)($class); + + return $handler instanceof ClassTransformer ? $handler : null; } } diff --git a/src/Transformer/ReflectionTransformer.php b/src/Transformer/ReflectionTransformer.php index 9abc5f90..a840656e 100644 --- a/src/Transformer/ReflectionTransformer.php +++ b/src/Transformer/ReflectionTransformer.php @@ -6,15 +6,24 @@ use Patchlevel\Hydrator\CircularReference; use Patchlevel\Hydrator\DenormalizationFailure; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\ClassMetadata; +use Patchlevel\Hydrator\Metadata\NestedObject; +use Patchlevel\Hydrator\Metadata\PropertyMetadata; use Patchlevel\Hydrator\NormalizationFailure; +use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; +use Patchlevel\Hydrator\Normalizer\Normalizer; use Patchlevel\Hydrator\TypeMismatch; use Throwable; use TypeError; use function array_key_exists; use function array_values; +use function assert; +use function is_array; +use function is_object; use function spl_object_id; /** @@ -24,10 +33,24 @@ */ final class ReflectionTransformer implements ClassTransformer { - /** @param ClassMetadata $metadata */ + /** @var array|null property name => nested object, resolved on the first call */ + private array|null $nested = null; + + /** @var array property name => handler of the nested class */ + private array $hydrateHandlers = []; + + /** @var array property name => handler of the nested class */ + private array $extractHandlers = []; + + /** + * @param ClassMetadata $metadata + * @param TransformerResolver|null $resolver of the hydrator, nested objects of its calls are mapped with the + * handlers of their classes directly + */ public function __construct( private readonly ClassMetadata $metadata, private readonly CallStack $callStack = new CallStack(), + private readonly TransformerResolver|null $resolver = null, ) { } @@ -50,6 +73,7 @@ public function hydrate(array $data, array $context): object } $constructorParameters = null; + $nested = $this->nested($context); foreach ($metadata->properties as $propertyMetadata) { if (!array_key_exists($propertyMetadata->fieldName, $data)) { @@ -74,7 +98,9 @@ public function hydrate(array $data, array $context): object if ($propertyMetadata->normalizer) { try { /** @psalm-suppress MixedAssignment */ - $value = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); + $value = isset($nested[$propertyMetadata->propertyName]) + ? $this->hydrateNested($propertyMetadata, $nested[$propertyMetadata->propertyName], $propertyMetadata->normalizer, $data[$propertyMetadata->fieldName], $context) + : $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); } catch (Throwable $e) { throw new DenormalizationFailure( $metadata->className, @@ -121,15 +147,15 @@ public function extract(object $object, array $context): array try { $data = []; + $nested = $this->nested($context); foreach ($this->metadata->properties as $propertyMetadata) { if ($propertyMetadata->normalizer) { try { /** @psalm-suppress MixedAssignment */ - $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( - $propertyMetadata->getValue($object), - $context, - ); + $data[$propertyMetadata->fieldName] = isset($nested[$propertyMetadata->propertyName]) + ? $this->extractNested($propertyMetadata, $nested[$propertyMetadata->propertyName], $propertyMetadata->normalizer, $propertyMetadata->getValue($object), $context) + : $propertyMetadata->normalizer->normalize($propertyMetadata->getValue($object), $context); } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -150,4 +176,108 @@ public function extract(object $object, array $context): array return $data; } + + /** + * The nested objects which are mapped with the handlers of their classes directly. Only for calls of the + * hydrator which created this transformer: a hydrator which wraps it has to see the nested objects. + * + * @param array $context + * + * @return array + */ + private function nested(array $context): array + { + if ($this->resolver === null) { + return []; + } + + if ($this->nested === null) { + $this->nested = []; + + foreach ($this->metadata->properties as $property) { + $nested = $property->nested(); + + if ($nested === null) { + continue; + } + + $this->nested[$property->propertyName] = $nested; + } + } + + if ($this->nested === [] || ($context[Hydrator::HYDRATOR] ?? null) !== $this->resolver->hydrator()) { + return []; + } + + return $this->nested; + } + + /** @param array $context */ + private function hydrateNested(PropertyMetadata $property, NestedObject $nested, Normalizer $normalizer, mixed $value, array $context): mixed + { + // null and wrong types are handled by the normalizer + if (!is_array($value) || $this->resolver === null) { + return $normalizer->denormalize($value, $context); + } + + $handler = $this->hydrateHandlers[$property->propertyName] + ??= $this->resolver->hydrateHandler($nested->className); + + if (!$nested->list) { + return $handler->hydrate($value, $context); + } + + assert($normalizer instanceof ArrayNormalizer); + $inner = $normalizer->innerNormalizer(); + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = is_array($item) ? $handler->hydrate($item, $context) : $inner->denormalize($item, $context); + } + + return $items; + } + + /** @param array $context */ + private function extractNested(PropertyMetadata $property, NestedObject $nested, Normalizer $normalizer, mixed $value, array $context): mixed + { + if ($this->resolver === null) { + return $normalizer->normalize($value, $context); + } + + if (!$nested->list) { + // subclasses are extracted with the handler of their own class by the hydrator + if (!is_object($value) || $value::class !== $nested->className) { + return $normalizer->normalize($value, $context); + } + + $handler = $this->extractHandlers[$property->propertyName] + ??= $this->resolver->extractHandler($nested->className); + + return $handler->extract($value, $context); + } + + if (!is_array($value)) { + return $normalizer->normalize($value, $context); + } + + assert($normalizer instanceof ArrayNormalizer); + $inner = $normalizer->innerNormalizer(); + $handler = null; + $items = []; + + foreach ($value as $key => $item) { + if (is_object($item) && $item::class === $nested->className) { + $handler ??= $this->extractHandlers[$property->propertyName] + ??= $this->resolver->extractHandler($nested->className); + $items[$key] = $handler->extract($item, $context); + + continue; + } + + $items[$key] = $inner->normalize($item, $context); + } + + return $items; + } } diff --git a/src/Transformer/ReflectionTransformerFactory.php b/src/Transformer/ReflectionTransformerFactory.php index 331c2271..ac1e86f8 100644 --- a/src/Transformer/ReflectionTransformerFactory.php +++ b/src/Transformer/ReflectionTransformerFactory.php @@ -25,6 +25,6 @@ public function __construct() */ public function create(ClassMetadata $metadata, TransformerResolver $resolver): ReflectionTransformer { - return new ReflectionTransformer($metadata, $this->callStack); + return new ReflectionTransformer($metadata, $this->callStack, $resolver); } } diff --git a/src/Transformer/TransformerResolver.php b/src/Transformer/TransformerResolver.php index ebfd2e68..c9f591c8 100644 --- a/src/Transformer/TransformerResolver.php +++ b/src/Transformer/TransformerResolver.php @@ -4,6 +4,8 @@ namespace Patchlevel\Hydrator\Transformer; +use Patchlevel\Hydrator\Handler\ExtractHandler; +use Patchlevel\Hydrator\Handler\HydrateHandler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Metadata\ClassNotFound; @@ -31,6 +33,30 @@ public function hydrator(): Hydrator; */ public function metadata(string $class): ClassMetadata; + /** + * What the hydrator does to hydrate an object of the class. Calling it behaves exactly like calling the hydrator, + * as long as the call came from {@see self::hydrator()}, without the detour through the normalizer and the + * hydrator itself. + * + * @param class-string $class + * + * @return ClassTransformer|HydrateHandler the transformer, if nothing else has to run for the class + * + * @throws ClassNotFound + */ + public function hydrateHandler(string $class): ClassTransformer|HydrateHandler; + + /** + * What the hydrator does to extract an object of exactly this class, see {@see self::hydrateHandler()}. + * + * @param class-string $class + * + * @return ClassTransformer|ExtractHandler the transformer, if nothing else has to run for the class + * + * @throws ClassNotFound + */ + public function extractHandler(string $class): ClassTransformer|ExtractHandler; + /** * The transformer the hydrator calls directly for the class in this direction: the class has no class * normalizer, is not lazy and no middleware runs for it. Mapping such a class in place behaves exactly like diff --git a/tests/Architecture/NoCodeGenerationAtRuntimeTest.php b/tests/Architecture/NoCodeGenerationAtRuntimeTest.php new file mode 100644 index 00000000..e5c1e07a --- /dev/null +++ b/tests/Architecture/NoCodeGenerationAtRuntimeTest.php @@ -0,0 +1,56 @@ +classes( + Selector::classname(GeneratedTransformerExtension::class), + Selector::classname(GeneratedTransformerFactory::class), + Selector::classname(GeneratedTransformer::class), + Selector::classname(TransformerFiles::class), + Selector::classname(ClassFingerprint::class), + ) + ->shouldNotDependOn() + ->classes( + Selector::classname(GeneratedTransformerWarmer::class), + Selector::classname(TransformerGenerator::class), + Selector::classname(ClassPlanner::class), + Selector::classname(ClassPlan::class), + Selector::classname(PropertyPlan::class), + Selector::classname(NestedPlan::class), + Selector::classname(ValueKind::class), + Selector::classname(CodeEmitter::class), + Selector::classname(Templates::class), + Selector::classname(GeneratedCode::class), + ); + } +} diff --git a/tests/Unit/Extension/Cryptography/CryptographyMiddlewareTest.php b/tests/Unit/Extension/Cryptography/CryptographyMiddlewareTest.php index 3f9d3105..56b49958 100644 --- a/tests/Unit/Extension/Cryptography/CryptographyMiddlewareTest.php +++ b/tests/Unit/Extension/Cryptography/CryptographyMiddlewareTest.php @@ -83,7 +83,7 @@ public function testSkipEncrypt(): void $otherMiddleware ->expects($this->once()) ->method('extract') - ->with($metadata, $object, [SubjectIds::class => new SubjectIds()], $next) + ->with($metadata, $object, [SubjectIds::class => new SubjectIds()], $this->isInstanceOf(Next::class)) ->willReturn($expected); $result = $middleware->extract( @@ -111,7 +111,7 @@ public function testEncrypt(): void $otherMiddleware ->expects($this->once()) ->method('extract') - ->with($metadata, $object, [SubjectIds::class => new SubjectIds(['default' => 'foo'])], $next) + ->with($metadata, $object, [SubjectIds::class => new SubjectIds(['default' => 'foo'])], $this->isInstanceOf(Next::class)) ->willReturn(['id' => 'foo', 'email' => 'info@patchlevel.de']); $cryptographer = $this->createMock(Cryptographer::class); @@ -149,7 +149,7 @@ public function testSkipDecrypt(): void $otherMiddleware ->expects($this->once()) ->method('hydrate') - ->with($metadata, $data, [SubjectIds::class => new SubjectIds()], $next) + ->with($metadata, $data, [SubjectIds::class => new SubjectIds()], $this->isInstanceOf(Next::class)) ->willReturn($expected); $middleware = new CryptographyMiddleware($cryptographer); diff --git a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php index f5db6fe3..ad21b270 100644 --- a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php +++ b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php @@ -52,6 +52,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated; use Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture; +use Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto; @@ -126,6 +127,7 @@ final class GeneratedStackHydratorTest extends TestCase PrivateChildDto::class, SensitiveDataProfileCreated::class, NestedLifecycleDto::class, + LinkedDto::class, ]; private static string $cachePath; @@ -252,6 +254,47 @@ public function testExtractCircularReference(): void $this->hydrator->extract($dto1); } + public function testExtractCircularReferenceThroughCustomNormalizer(): void + { + $first = new LinkedDto('first'); + $second = new LinkedDto('second', $first); + $first->next = $second; + + $this->expectException(CircularReference::class); + $this->expectExceptionMessage('Circular reference detected: Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto -> Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto -> Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto'); + + $this->hydrator->extract($first); + } + + public function testExtractLinkedObjectsWithCustomNormalizer(): void + { + $data = ['name' => 'first', 'next' => ['name' => 'second', 'next' => null]]; + + self::assertSame($data, $this->hydrator->extract($this->hydrator->hydrate(LinkedDto::class, $data))); + } + + public function testCircularReferenceCheckCanBeDisabled(): void + { + $tracked = static function (StackHydrator $hydrator, string $class): bool { + $transformer = self::transformers($hydrator)[$class] ?? null; + self::assertInstanceOf(GeneratedTransformer::class, $transformer); + + return $transformer->tracked; + }; + + $data = ['name' => 'first', 'next' => ['name' => 'second', 'next' => null]]; + + $this->hydrator->extract($this->hydrator->hydrate(LinkedDto::class, $data)); + self::assertTrue($tracked($this->hydrator, LinkedDto::class)); + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension(self::$cachePath, circularReferenceCheck: false)) + ->build(); + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(LinkedDto::class, $data))); + self::assertFalse($tracked($hydrator, LinkedDto::class)); + } + public function testExtractWithInferNormalizer(): void { $result = $this->hydrator->extract( diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php index 01ad4728..8228a4fd 100644 --- a/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -96,7 +104,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -113,7 +128,14 @@ public function extract(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -136,7 +158,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['to']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -173,7 +197,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -190,7 +221,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -205,16 +243,16 @@ public function extractNested(object $object, array $context): array protected function initialize(): void { $this->n0 = $this->normalizer('to'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php index 2a73e5aa..07b47686 100644 --- a/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -96,7 +104,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -113,7 +128,14 @@ public function extract(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -136,7 +158,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['to']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -173,7 +197,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -190,7 +221,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -205,16 +243,16 @@ public function extractNested(object $object, array $context): array protected function initialize(): void { $this->n0 = $this->normalizer('to'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php index cda39f0a..316f5f1d 100644 --- a/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -96,7 +104,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -113,7 +128,14 @@ public function extract(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -136,7 +158,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['to']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -173,7 +197,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -190,7 +221,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->to; - $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto) { + $v_to = $this->he0->extract($value, $context); + } else { + $v_to = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -205,16 +243,16 @@ public function extractNested(object $object, array $context): array protected function initialize(): void { $this->n0 = $this->normalizer('to'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php index 3d024740..d46963b9 100644 --- a/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -119,7 +127,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->email; - $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->he0->extract($value, $context); + } else { + $v_email = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -138,7 +153,14 @@ public function extract(object $object, array $context): array try { $value = $object->email; - $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->he0->extract($value, $context); + } else { + $v_email = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -183,7 +205,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['email']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -222,7 +246,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->email; - $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->he0->extract($value, $context); + } else { + $v_email = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -241,7 +272,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->email; - $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email) { + $v_email = $this->he0->extract($value, $context); + } else { + $v_email = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -259,16 +297,16 @@ protected function initialize(): void { $this->d0 = $this->promotedDefault('email'); $this->n0 = $this->normalizer('email'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php index 56c1a575..0335bde5 100644 --- a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php @@ -1,7 +1,7 @@ ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'name', $e); + } + } + + if (\array_key_exists('next', $data)) { + try { + $value = $this->n0->denormalize($data['next'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + try { + $object->next = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $e); + } + } else { + $object->next = NULL; + } + + return $object; + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class; + + try { + try { + $v_next = $this->n0->normalize($object->next, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + return [ + 'name' => $object->name, + 'next' => $v_next, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_next = $this->n0->normalize($object->next, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + return [ + 'name' => $object->name, + 'next' => $v_next, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'name', $e); + } + } + + if (\array_key_exists('next', $data)) { + try { + $value = $this->n0->denormalize($data['next'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + try { + $object->next = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $e); + } + } else { + $object->next = NULL; + } + + return $object; + } + + public function extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class; + + try { + try { + $v_next = $this->n0->normalize($object->next, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + return [ + 'name' => $object->name, + 'next' => $v_next, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_next = $this->n0->normalize($object->next, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LinkedDto::class, 'next', $this->n0::class, $e); + } + + return [ + 'name' => $object->name, + 'next' => $v_next, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('next'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php index 767ed875..c67e3577 100644 --- a/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -81,8 +93,22 @@ public function hydrate(array $data, array $context): object if ($this->ih1 && $inline && \is_array($value)) { $items = []; + if ($this->nh1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? ($this->nh1)($item, $context) : $this->n2->denormalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hh1->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + } + + $value = $items; + } elseif ($this->hh1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + $items[$key] = \is_array($item) ? $this->hh1->hydrate($item, $context) : $this->n2->denormalize($item, $context); } $value = $items; @@ -128,7 +154,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->child; - $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->he0->extract($value, $context); + } else { + $v_child = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -141,8 +174,22 @@ public function extract(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? ($this->ne1)($item, $context) : $this->n2->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + } + + $v_items = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extract($item, $context) : $this->n2->normalize($item, $context); } $v_items = $items; @@ -166,7 +213,14 @@ public function extract(object $object, array $context): array try { $value = $object->child; - $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->he0->extract($value, $context); + } else { + $v_child = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -179,8 +233,22 @@ public function extract(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? ($this->ne1)($item, $context) : $this->n2->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + } + + $v_items = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extract($item, $context) : $this->n2->normalize($item, $context); } $v_items = $items; @@ -210,7 +278,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['child']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -232,8 +302,22 @@ public function hydrateNested(array $data, array $context): object if ($this->ih1 && $inline && \is_array($value)) { $items = []; + if ($this->nh1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? ($this->nh1)($item, $context) : $this->n2->denormalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hh1->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + } + + $value = $items; + } elseif ($this->hh1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + $items[$key] = \is_array($item) ? $this->hh1->hydrate($item, $context) : $this->n2->denormalize($item, $context); } $value = $items; @@ -275,7 +359,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->child; - $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->he0->extract($value, $context); + } else { + $v_child = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -288,8 +379,22 @@ public function extractNested(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? ($this->ne1)($item, $context) : $this->n2->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + } + + $v_items = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extract($item, $context) : $this->n2->normalize($item, $context); } $v_items = $items; @@ -313,7 +418,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->child; - $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture) { + $v_child = $this->he0->extract($value, $context); + } else { + $v_child = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -326,8 +438,22 @@ public function extractNested(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? ($this->ne1)($item, $context) : $this->n2->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + } + + $v_items = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->he1->extract($item, $context) : $this->n2->normalize($item, $context); } $v_items = $items; @@ -349,24 +475,25 @@ public function extractNested(object $object, array $context): array protected function initialize(): void { $this->n0 = $this->normalizer('child'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; $this->n1 = $this->normalizer('items'); - $h1 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); - $e1 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); - $this->ih1 = $h1 !== null; - $this->ie1 = $e1 !== null; - if ($this->ih1 || $this->ie1) { $this->x0 = $h1 ?? $e1; } - if ($this->ih1 || $this->ie1) { $this->n2 = $this->n1->innerNormalizer(); } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh1 = $this->handler($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); + $this->he1 = $this->handler($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); + $this->ih1 = $this->hh1 instanceof GeneratedTransformer; + $this->ie1 = $this->he1 instanceof GeneratedTransformer; + if ($this->hh1 !== null || $this->he1 !== null) { $this->n2 = $this->n1->innerNormalizer(); } + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } + if ($this->ih1) { $this->hh1->init(); $this->nh1 = $this->hh1->nestedHydrator; } + if ($this->ie1) { $this->he1->init(); $this->ne1 = $this->he1->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)) - || ($this->ie1 ? $this->x0->recursive() : self::mayRecurse($this->n1)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)) + || ($this->ie1 ? $this->he1->recursive() : self::mayRecurse($this->n1)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php index 755a59ef..0d8f02cf 100644 --- a/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n1->denormalize($value, $context); } @@ -148,8 +159,22 @@ public function hydrate(array $data, array $context): object if ($this->ih1 && $inline && \is_array($value)) { $items = []; + if ($this->nh1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? ($this->nh1)($item, $context) : $this->n6->denormalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hh1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + } + + $value = $items; + } elseif ($this->hh1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + $items[$key] = \is_array($item) ? $this->hh1->hydrate($item, $context) : $this->n6->denormalize($item, $context); } $value = $items; @@ -215,7 +240,14 @@ public function extract(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n1->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -252,8 +284,22 @@ public function extract(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? ($this->ne1)($item, $context) : $this->n6->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + } + + $v_skillsHashMap = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extract($item, $context) : $this->n6->normalize($item, $context); } $v_skillsHashMap = $items; @@ -290,7 +336,14 @@ public function extract(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n1->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -327,8 +380,22 @@ public function extract(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? ($this->ne1)($item, $context) : $this->n6->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + } + + $v_skillsHashMap = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extract($item, $context) : $this->n6->normalize($item, $context); } $v_skillsHashMap = $items; @@ -377,7 +444,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['profileCreated']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n1->denormalize($value, $context); } @@ -447,8 +516,22 @@ public function hydrateNested(array $data, array $context): object if ($this->ih1 && $inline && \is_array($value)) { $items = []; + if ($this->nh1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? ($this->nh1)($item, $context) : $this->n6->denormalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hh1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + } + + $value = $items; + } elseif ($this->hh1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + $items[$key] = \is_array($item) ? $this->hh1->hydrate($item, $context) : $this->n6->denormalize($item, $context); } $value = $items; @@ -510,7 +593,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n1->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -547,8 +637,22 @@ public function extractNested(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? ($this->ne1)($item, $context) : $this->n6->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + } + + $v_skillsHashMap = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extract($item, $context) : $this->n6->normalize($item, $context); } $v_skillsHashMap = $items; @@ -585,7 +689,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n1->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -622,8 +733,22 @@ public function extractNested(object $object, array $context): array if ($this->ie1 && $inline && \is_array($value)) { $items = []; + if ($this->ne1 !== null) { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? ($this->ne1)($item, $context) : $this->n6->normalize($item, $context); + } + } else { + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + } + + $v_skillsHashMap = $items; + } elseif ($this->he1 !== null && $inline && \is_array($value)) { + $items = []; + foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->he1->extract($item, $context) : $this->n6->normalize($item, $context); } $v_skillsHashMap = $items; @@ -651,32 +776,32 @@ protected function initialize(): void { $this->n0 = $this->normalizer('status'); $this->n1 = $this->normalizer('profileCreated'); - $h0 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Hydrate); - $e0 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + $this->hh0 = $this->handler($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; $this->n2 = $this->normalizer('defaultArray'); $this->n3 = $this->normalizer('listArray'); $this->n4 = $this->normalizer('iterableArray'); $this->n5 = $this->normalizer('skillsHashMap'); - $h1 = $this->nested($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Hydrate); - $e1 = $this->nested($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Extract); - $this->ih1 = $h1 !== null; - $this->ie1 = $e1 !== null; - if ($this->ih1 || $this->ie1) { $this->x1 = $h1 ?? $e1; } - if ($this->ih1 || $this->ie1) { $this->n6 = $this->n5->innerNormalizer(); } - if (isset($this->x0)) { $this->x0->init(); } - if (isset($this->x1)) { $this->x1->init(); } + $this->hh1 = $this->handler($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Hydrate); + $this->he1 = $this->handler($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Extract); + $this->ih1 = $this->hh1 instanceof GeneratedTransformer; + $this->ie1 = $this->he1 instanceof GeneratedTransformer; + if ($this->hh1 !== null || $this->he1 !== null) { $this->n6 = $this->n5->innerNormalizer(); } + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } + if ($this->ih1) { $this->hh1->init(); $this->nh1 = $this->hh1->nestedHydrator; } + if ($this->ie1) { $this->he1->init(); $this->ne1 = $this->he1->nestedExtractor; } } protected function mayReachItself(): bool { return self::mayRecurse($this->n0) - || ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n1)) + || ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n1)) || self::mayRecurse($this->n2) || self::mayRecurse($this->n3) || self::mayRecurse($this->n4) - || ($this->ie1 ? $this->x1->recursive() : self::mayRecurse($this->n5)); + || ($this->ie1 ? $this->he1->recursive() : self::mayRecurse($this->n5)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php index 6b4d7598..a2839ecd 100644 --- a/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php @@ -1,7 +1,7 @@ reflection->newInstanceWithoutConstructor(); - - $inline = true; - - return ($this->h)($object, $data, $context, $inline); + return ($this->nestedHydrator)($data, $context); } public function extractNested(object $object, array $context): array { - $inline = true; - - return ($this->e)($object, $context, $inline); + return ($this->nestedExtractor)($object, $context); } protected function initialize(): void @@ -194,6 +191,93 @@ protected function initialize(): void throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); } + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->nestedHydrator = Closure::bind(function (array $data, array $context): object { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'name', $e); + } + } + + ($this->hp0)($object, $data, $context, $inline); + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->nestedExtractor = Closure::bind(function (object $object, array $context): array { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class; + + try { + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + return [ 'profileId' => $v_profileId, 'name' => $object->name, diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php index 98caadf8..65ef12b1 100644 --- a/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php @@ -1,7 +1,7 @@ reflection->newInstanceWithoutConstructor(); - - $inline = true; - - return ($this->h)($object, $data, $context, $inline); + return ($this->nestedHydrator)($data, $context); } public function extractNested(object $object, array $context): array { - $inline = true; - - return ($this->e)($object, $context, $inline); + return ($this->nestedExtractor)($object, $context); } protected function initialize(): void @@ -161,6 +158,93 @@ protected function initialize(): void throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); } + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->nestedHydrator = Closure::bind(function (array $data, array $context): object { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'name', $e); + } + } + + if (\array_key_exists('age', $data)) { + $value = $data['age']; + + try { + $object->age = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'age', $e); + } + } else { + $object->age = 42; + } + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->nestedExtractor = Closure::bind(function (object $object, array $context): array { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class; + + try { + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + return [ 'profileId' => $v_profileId, 'name' => $object->name, diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php index a1e00e1c..80726963 100644 --- a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php @@ -1,7 +1,7 @@ ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -96,7 +104,14 @@ public function extract(object $object, array $context): array try { try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -113,7 +128,14 @@ public function extract(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -136,7 +158,9 @@ public function hydrateNested(array $data, array $context): object $value = $data['event']; if ($this->ih0 && $inline && \is_array($value)) { - $value = $this->x0->hydrateNested($value, $context); + $value = $this->nh0 !== null ? ($this->nh0)($value, $context) : $this->hh0->hydrateNested($value, $context); + } elseif ($this->hh0 !== null && $inline && \is_array($value)) { + $value = $this->hh0->hydrate($value, $context); } else { $value = $this->n0->denormalize($value, $context); } @@ -173,7 +197,14 @@ public function extractNested(object $object, array $context): array try { try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -190,7 +221,14 @@ public function extractNested(object $object, array $context): array try { $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + + if ($this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->ne0 !== null ? ($this->ne0)($value, $context) : $this->he0->extractNested($value, $context); + } elseif ($this->he0 !== null && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated) { + $v_profileCreated = $this->he0->extract($value, $context); + } else { + $v_profileCreated = $this->n0->normalize($value, $context); + } } catch (CircularReference $e) { throw $e; } catch (Throwable $e) { @@ -205,16 +243,16 @@ public function extractNested(object $object, array $context): array protected function initialize(): void { $this->n0 = $this->normalizer('profileCreated'); - $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Hydrate); - $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Extract); - $this->ih0 = $h0 !== null; - $this->ie0 = $e0 !== null; - if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } - if (isset($this->x0)) { $this->x0->init(); } + $this->hh0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Hydrate); + $this->he0 = $this->handler($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Extract); + $this->ih0 = $this->hh0 instanceof GeneratedTransformer; + $this->ie0 = $this->he0 instanceof GeneratedTransformer; + if ($this->ih0) { $this->hh0->init(); $this->nh0 = $this->hh0->nestedHydrator; } + if ($this->ie0) { $this->he0->init(); $this->ne0 = $this->he0->nestedExtractor; } } protected function mayReachItself(): bool { - return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + return ($this->ie0 ? $this->he0->recursive() : self::mayRecurse($this->n0)); } } \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php index 8d547a87..2c0d8349 100644 --- a/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php +++ b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php @@ -1,7 +1,7 @@ $context */ + public function normalize(mixed $value, array $context): mixed + { + if (!$value instanceof LinkedDto) { + return null; + } + + $hydrator = $context[Hydrator::HYDRATOR] ?? null; + + if (!$hydrator instanceof Hydrator) { + throw new MissingHydrator(); + } + + return $hydrator->extract($value, $context); + } + + /** @param array $context */ + public function denormalize(mixed $value, array $context): mixed + { + if (!is_array($value)) { + return null; + } + + $hydrator = $context[Hydrator::HYDRATOR] ?? null; + + if (!$hydrator instanceof Hydrator) { + throw new MissingHydrator(); + } + + return $hydrator->hydrate(LinkedDto::class, $value, $context); + } +} diff --git a/tests/Unit/Fixture/LinkedDto.php b/tests/Unit/Fixture/LinkedDto.php new file mode 100644 index 00000000..b491bff3 --- /dev/null +++ b/tests/Unit/Fixture/LinkedDto.php @@ -0,0 +1,16 @@ +direct(ProfileId::class, Direction::Hydrate)); self::assertNull($resolver->direct(ProfileId::class, Direction::Extract)); + + // the handlers do what the hydrator does for the class + self::assertSame($resolver->direct(ProfileCreated::class, Direction::Hydrate), $resolver->hydrateHandler(ProfileCreated::class)); + self::assertInstanceOf(HydrateHandler::class, $resolver->hydrateHandler(Skill::class)); + self::assertEquals(new Skill('php'), $resolver->hydrateHandler(Skill::class)->hydrate(['name' => 'php'], [])); + self::assertSame(['name' => 'php'], $resolver->extractHandler(Skill::class)->extract(new Skill('php'), [])); + self::assertEquals(ProfileId::fromString('1'), $resolver->hydrateHandler(ProfileId::class)->hydrate('1', [])); + self::assertSame('1', $resolver->extractHandler(ProfileId::class)->extract(ProfileId::fromString('1'), [])); } #[RequiresPhp('>=8.4')]