diff --git a/Makefile b/Makefile index 9c351c11..f480c35a 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ static: phpstan cs ## run stat test: phpunit ## run tests .PHONY: snapshot -snapshot: vendor ## regenerate the snapshot of the generated middleware +snapshot: vendor ## regenerate the snapshots of the generated transformers UPDATE_SNAPSHOTS=1 vendor/bin/phpunit --no-coverage --filter testGeneratedCodeMatchesSnapshot .PHONY: benchmark diff --git a/docs/extensions.md b/docs/extensions.md index 6d8bfc7b..3992db4d 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -27,19 +27,20 @@ The library ships with six extensions out of the box: | Extension | Purpose | | --- | --- | -| `CoreExtension` | The default behaviour, the `TransformMiddleware` and the `BuiltInGuesser`. | +| `CoreExtension` | The default behaviour, the `BuiltInGuesser` for enums, dates and nested objects. | | `LifecycleExtension` | [Lifecycle hooks](lifecycle-hooks.md), run code before and after the extract and hydrate process. | | `CryptographyExtension` | [Cryptography](cryptography.md), encrypt and decrypt sensitive data with crypto-shredding. | | `UpcastExtension` | [Upcasting](upcasting.md), reshape outdated stored data while it is hydrated. | | `TracingExtension` | [Tracing](tracing.md), measure every hydrate and extract call. | -| `GeneratedMiddlewareExtension` | [Generated code](generated-code.md), generated mapping code for a fixed set of classes on top of the `CoreExtension`. | +| `GeneratedTransformerExtension` | [Generated code](generated-code.md), generated mapping code instead of reflection on top of the `CoreExtension`. | ## Middleware A middleware wraps the hydration and extraction process, similar to HTTP middlewares. It can modify the incoming data, the outgoing array or the object -itself, and then delegates to the next middleware on the stack. The innermost -middleware is the `TransformMiddleware`, which does the actual property mapping. +itself, and then delegates to the next middleware on the stack. At the end of +the stack the hydrator does the actual property mapping with a +[transformer](#transformer-factories). ```php use Patchlevel\Hydrator\Metadata\ClassMetadata; @@ -62,8 +63,7 @@ final class RemoveNullValuesMiddleware implements Middleware } ``` Middlewares are added with a priority, higher priorities run first (outermost). -The `TransformMiddleware` from the `CoreExtension` has priority `-64`, so it -always runs last. +The transformation always runs last, after all middlewares. ```php $builder->addMiddleware(new RemoveNullValuesMiddleware(), 0); @@ -115,9 +115,9 @@ upcaster is a `CallbackUpcaster` or carries an [`#[UpcasterFor]`](upcasting.md#writing-an-upcaster) attribute, it is also left out while hydrating classes none of them target. -:::warning -At least one middleware has to run. If every middleware skips a class, an -`AllMiddlewaresSkipped` exception is thrown. +:::tip +If every middleware skips a class, the hydrator calls the transformer directly +without building the middleware stack. ::: ## Metadata enricher @@ -154,18 +154,57 @@ extras) can be [cached](caching.md), everything you store in `extras` must be serializable. ::: +## Transformer factories + +At the end of the stack the hydrator turns the array into the object and back +with the `ClassTransformer` of the class. By default this is the +`ReflectionTransformer`. +A transformer factory can provide another transformer for a class, the +[generated code](generated-code.md) extension uses this to replace reflection +with generated code. + +```php +use Patchlevel\Hydrator\Metadata\ClassMetadata; +use Patchlevel\Hydrator\StackHydrator; +use Patchlevel\Hydrator\Transformer\ClassTransformer; +use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; + +final class MoneyTransformerFactory implements ClassTransformerFactory +{ + public function create(ClassMetadata $metadata, StackHydrator $hydrator): ClassTransformer|null + { + if ($metadata->className !== Money::class) { + return null; + } + + return new MoneyTransformer(); + } +} +``` +```php +$builder->addTransformerFactory(new MoneyTransformerFactory()); +``` +The factories are asked once per class, the first transformer wins and is +cached by the hydrator. If no factory returns a transformer, reflection is +used. Factories also accept a priority, a factory with a higher priority is +asked first. + +:::tip +If no middleware has to run for a class, because there is none or all of them +[skip](#skipping-middlewares) it, the hydrator calls the transformer directly +without building the middleware stack. +::: + ## Decorators A decorator wraps the whole hydrator instead of a single step inside the stack. Use it for things which concern the call as a whole, like [tracing](tracing.md) or logging. A decorator implements `HydratorDecorator` -and returns a new `Hydrator` around the given one. It also receives the -innermost `StackHydrator`, in case it needs its metadata. +and returns a new `Hydrator` around the given one. ```php use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\HydratorDecorator; -use Patchlevel\Hydrator\StackHydrator; final class AuditDecorator implements HydratorDecorator { @@ -174,7 +213,7 @@ final class AuditDecorator implements HydratorDecorator ) { } - public function decorate(Hydrator $hydrator, StackHydrator $stack): Hydrator + public function decorate(Hydrator $hydrator): Hydrator { return new AuditHydrator($hydrator, $this->auditLog); } diff --git a/docs/generated-code.md b/docs/generated-code.md index 54e2d3b2..08633e07 100644 --- a/docs/generated-code.md +++ b/docs/generated-code.md @@ -1,113 +1,106 @@ # Generated Code -The `TransformMiddleware` maps arrays to objects with reflection: it iterates -over the metadata of a class, sets each property through a `ReflectionProperty` -and calls the [normalizers](normalizer.md) dynamically. This is fast enough for -most applications, but when you hydrate millions of objects the reflection -overhead adds up. The `GeneratedMiddlewareExtension` generates plain PHP code -for a fixed set of classes, which is then used instead of the reflection based -mapping for exactly these classes. +By default the `ReflectionTransformer` maps arrays to objects with reflection: +it iterates over the metadata of a class, sets each property through a +`ReflectionProperty` and calls the [normalizers](normalizer.md) dynamically. +This is fast enough for most applications, but when you hydrate millions of +objects the reflection overhead adds up. The `GeneratedTransformerExtension` +uses generated plain PHP code instead, one class per hydrated class. ## Enable the extension -Register the `GeneratedMiddlewareExtension` in addition to the `CoreExtension`. -It needs a writable directory for the generated file and the list of classes -for which code should be generated. Classes referenced by these classes, such -as nested value objects, are discovered automatically. +Register the `GeneratedTransformerExtension` in addition to the `CoreExtension`. +It needs a directory where the generated transformers are stored. ```php use Patchlevel\Hydrator\CoreExtension; -use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; use Patchlevel\Hydrator\StackHydratorBuilder; $hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension( + ->useExtension(new GeneratedTransformerExtension( cachePath: __DIR__ . '/var/cache/hydrator', - classes: [ - ProfileCreated::class, - NameChanged::class, - ], + autoGenerate: $debug, )) ->buildHydrator(); ``` -:::note -The extension registers a [decorator](extensions.md#decorators) around the -`StackHydrator`. For the generated classes it calls the generated code directly -and skips the middleware stack entirely, as long as no other middleware has to -run for the class (see [Nested objects](#nested-objects) for how that is -decided). Everything else is passed on to the `StackHydrator`. The decorator -has the lowest priority, so other decorators like [tracing](tracing.md) are -always wrapped around it. -::: - -:::warning -The extension requires `buildHydrator()`. The deprecated `build()` can not -apply decorators and throws a `DecoratorsNotApplied` exception. -::: +The extension registers a [transformer factory](extensions.md#transformer-factories), +nothing else changes: all middlewares run as before and the generated code +takes the place of reflection at the end of the stack. If no other middleware +has to run for a class, the hydrator calls the generated code directly. :::success -The generated code behaves exactly like the `TransformMiddleware`: objects are +The generated code behaves exactly like the `ReflectionTransformer`: objects are created without calling the constructor, missing fields keep their promoted default value, private and readonly properties are supported and the same exceptions are thrown. You can switch between both without changing your classes. ::: -The generated middleware runs right before the `TransformMiddleware` of the -`CoreExtension`, regardless of the registration order. The code is generated -when the hydrator is built, so it always matches the metadata of all registered -extensions. Classes which are not part of the list are passed on and handled by -the `TransformMiddleware` as usual. +## Generate the code -## Caching - -The generated middleware is written to the cache path once and reused on -subsequent requests, so the generation only happens on the first run. The file -name is derived from the configuration, changing the class list or the options -results in a new file. In development you can regenerate the code on every -build with the `debug` flag. +With `autoGenerate` the transformer of a class is generated the first time the +class is hydrated or extracted. This is convenient in development, but in +production the cache path is often read-only. There you generate the code +ahead of time, for example when the application is deployed, with the +`TransformerCompiler`. Use the metadata factory of the builder, so the code is +generated with the guessers and enrichers of all extensions. ```php -use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; +use Patchlevel\Hydrator\CoreExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\TransformerCompiler; +use Patchlevel\Hydrator\StackHydratorBuilder; -$extension = new GeneratedMiddlewareExtension( - cachePath: __DIR__ . '/var/cache/hydrator', - classes: [ProfileCreated::class], - debug: true, -); +$builder = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension(__DIR__ . '/var/cache/hydrator')); + +$compiler = new TransformerCompiler($builder->metadataFactory(), __DIR__ . '/var/cache/hydrator'); +$compiler->compile([ + ProfileCreated::class, + NameChanged::class, +]); ``` -:::warning -The generated code is tied to the [metadata](caching.md) and the definition of -your classes. If a field is renamed, a normalizer is added, a property changes -its visibility or a default value changes after the code was generated, the -middleware throws an `OutdatedGeneratedMiddleware` exception instead of running -the outdated code. Clear the cache path when you deploy, like you do for other -generated files. +Classes referenced by these classes, such as nested value objects, are +discovered and generated automatically. + +:::note +Classes without generated code are transformed with reflection. A missing +compile step makes the hydrator slower, but never breaks it. ::: +## Outdated code + +The generated code is tied to the [metadata](caching.md) and the definition of +your classes. The name of a generated transformer contains a fingerprint of +the class: its properties, field names, normalizers, visibility and defaults. +If one of them changes, the existing code no longer matches and the class is +transformed with reflection until its code is generated again. Old files are +never used again, you can delete them when you deploy. + ## Nested objects The biggest speedup comes from inlining nested objects: instead of going through -the whole middleware stack again for every nested object, the generated code -hydrates and extracts them directly. Inlining a class would hide it from the +the hydrator again for every nested object, the generated code hydrates and +extracts them directly. Inlining a class would hide it from the other middlewares, so it only happens when every other middleware in the stack -is a `SkippableMiddleware` which [skips](extensions.md) that class. The decision -is made per class and per direction: with the +is a `SkippableMiddleware` which [skips](extensions.md#skipping-middlewares) +that class. The decision is made per class and per direction: with the [cryptography](cryptography.md) extension for example, value objects without sensitive data are inlined while classes with sensitive data still go through the stack. Inlining would also hide nested objects from [decorators](extensions.md#decorators), -so nothing is inlined as soon as another decorator is registered. With the +so nothing is inlined as soon as a decorator is registered. With the [tracing](tracing.md) extension, every nested object goes through the tracer and shows up in the traces, at the cost of the inlining. :::note -Nested classes with a class level normalizer or [lazy](lazy.md) classes are -never inlined, they always take the regular path through the hydrator. The -generated middleware is itself skippable: classes it has no code for go -straight to the `TransformMiddleware`. +Nested classes with a class level normalizer, [lazy](lazy.md) classes and +nested classes which changed since the code was generated are never inlined, +they always take the regular path through the hydrator. ::: ## Learn more diff --git a/docs/hydrator.md b/docs/hydrator.md index 32b3553a..cc26f289 100644 --- a/docs/hydrator.md +++ b/docs/hydrator.md @@ -8,9 +8,8 @@ resolves [normalizers](normalizer.md) from metadata. ## Create the hydrator The recommended way is the `StackHydratorBuilder` with the `CoreExtension`. -The `CoreExtension` registers the `TransformMiddleware`, which does the actual -property mapping, and the `BuiltInGuesser`, which picks normalizers for enums, -date types and nested objects. +The `CoreExtension` registers the `BuiltInGuesser`, which picks normalizers for +enums, date types and nested objects. ```php use Patchlevel\Hydrator\CoreExtension; @@ -28,7 +27,7 @@ works as long as no decorator is registered. ::: If you don't need any extensions, you can also instantiate the `StackHydrator` -directly, it defaults to the same middleware and guesser: +directly, it behaves like the builder with only the `CoreExtension`: ```php use Patchlevel\Hydrator\StackHydrator; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2fd04d44..cef101c8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -97,16 +97,16 @@ parameters: path: src/Metadata/ClassMetadata.php - - message: '#^Method Patchlevel\\Hydrator\\Middleware\\TransformMiddleware\:\:hydrate\(\) should return T of object but returns mixed\.$#' + message: '#^Method Patchlevel\\Hydrator\\Transformer\\ReflectionTransformer\:\:hydrate\(\) should return T of object but returns mixed\.$#' identifier: return.type count: 1 - path: src/Middleware/TransformMiddleware.php + path: src/Transformer/ReflectionTransformer.php - message: '#^Parameter \#1 \$object of method Patchlevel\\Hydrator\\Metadata\\PropertyMetadata\:\:setValue\(\) expects object, mixed given\.$#' identifier: argument.type count: 2 - path: src/Middleware/TransformMiddleware.php + path: src/Transformer/ReflectionTransformer.php - message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\\|null\) does not accept string\.$#' @@ -121,9 +121,9 @@ parameters: path: src/Normalizer/ObjectNormalizer.php - - message: '#^Method Patchlevel\\Hydrator\\StackHydrator\:\:middlewaresFor\(\) should return non\-empty\-list\ but returns list\\.$#' - identifier: return.type - count: 1 + message: '#^Parameter \#1 \$data of method Patchlevel\\Hydrator\\Transformer\\ClassTransformer\:\:hydrate\(\) expects array\, array\ given\.$#' + identifier: argument.type + count: 3 path: src/StackHydrator.php - diff --git a/src/CoreExtension.php b/src/CoreExtension.php index 04074cac..ecb9bd3a 100644 --- a/src/CoreExtension.php +++ b/src/CoreExtension.php @@ -5,13 +5,11 @@ namespace Patchlevel\Hydrator; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; -use Patchlevel\Hydrator\Middleware\TransformMiddleware; final class CoreExtension implements Extension { public function configure(StackHydratorBuilder $builder): void { - $builder->addMiddleware(new TransformMiddleware(), Extension::PRIORITY_TRANSFORM); $builder->addGuesser(new BuiltInGuesser(), -64); } } diff --git a/src/Extension.php b/src/Extension.php index e9fb4e9b..8a8c221a 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -15,7 +15,11 @@ interface Extension /** Last structural step before the array becomes an object. */ public const PRIORITY_BEFORE_TRANSFORM = 0; - /** Build the object from the array and deconstruct it again. */ + /** + * Build the object from the array and deconstruct it again. + * + * @deprecated the hydrator transforms the data at the end of the stack, use a transformer factory to change it + */ public const PRIORITY_TRANSFORM = -64; public function configure(StackHydratorBuilder $builder): void; diff --git a/src/Extension/Generated/ClassFingerprint.php b/src/Extension/Generated/ClassFingerprint.php index c460c55a..4b12a03e 100644 --- a/src/Extension/Generated/ClassFingerprint.php +++ b/src/Extension/Generated/ClassFingerprint.php @@ -5,21 +5,33 @@ namespace Patchlevel\Hydrator\Extension\Generated; use Patchlevel\Hydrator\Metadata\ClassMetadata; +use Patchlevel\Hydrator\Metadata\ClassNotFound; +use Patchlevel\Hydrator\Metadata\MetadataFactory; +use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; +use Patchlevel\Hydrator\Normalizer\InvalidType; +use Patchlevel\Hydrator\Normalizer\Normalizer; +use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; +use function array_pop; use function hash; +use function ksort; use function serialize; use function substr; /** * Fingerprint of everything the generated code of a class depends on: the properties with their field names, - * normalizers, visibility, declaring class and inlined defaults. The generated middleware compares it with the - * current metadata on initialization and refuses to run with outdated code. + * normalizers, visibility, declaring class and inlined defaults. The fingerprints of a class and its nested classes are + * part of the name of the generated transformer, and nested classes are only inlined if their fingerprint still matches. * * @internal */ final class ClassFingerprint { - /** @param ClassMetadata $metadata */ + /** + * @param ClassMetadata $metadata + * + * @template T of object + */ public static function of(ClassMetadata $metadata): string { $defaults = $metadata->promotedConstructorDefaults(); @@ -49,4 +61,65 @@ public static function of(ClassMetadata $metadata): string return substr(hash('sha256', serialize($shape)), 0, 16); } + + /** + * Fingerprint of the class together with every class reachable through object normalizers, since the generated + * code of a class contains the inlined code of its nested classes. + * + * @param ClassMetadata $metadata + * + * @template T of object + */ + public static function ofGraph(ClassMetadata $metadata, MetadataFactory $metadataFactory): string + { + /** @var array $fingerprints */ + $fingerprints = []; + $queue = [$metadata]; + + while ($queue !== []) { + $current = array_pop($queue); + + if (isset($fingerprints[$current->className])) { + continue; + } + + $fingerprints[$current->className] = self::of($current); + + foreach ($current->properties as $property) { + $nested = self::nestedClass($property->normalizer); + + if ($nested === null || isset($fingerprints[$nested])) { + continue; + } + + try { + $queue[] = $metadataFactory->metadata($nested); + } catch (ClassNotFound) { + continue; + } + } + } + + ksort($fingerprints); + + return substr(hash('sha256', serialize($fingerprints)), 0, 16); + } + + /** @return class-string|null */ + private static function nestedClass(Normalizer|null $normalizer): string|null + { + if ($normalizer instanceof ArrayNormalizer) { + $normalizer = $normalizer->innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + return $normalizer->className(); + } catch (InvalidType) { + return null; + } + } } diff --git a/src/Extension/Generated/ClassNotGeneratable.php b/src/Extension/Generated/ClassNotGeneratable.php index bff6fb12..8b4f821c 100644 --- a/src/Extension/Generated/ClassNotGeneratable.php +++ b/src/Extension/Generated/ClassNotGeneratable.php @@ -13,6 +13,6 @@ final class ClassNotGeneratable extends InvalidArgumentException implements Hydr { public function __construct(string $class, string $reason) { - parent::__construct(sprintf('A middleware for class "%s" can not be generated: %s', $class, $reason)); + parent::__construct(sprintf('A transformer for class "%s" can not be generated: %s', $class, $reason)); } } diff --git a/src/Extension/Generated/ClassPlan.php b/src/Extension/Generated/ClassPlan.php index c744ff18..c28f745b 100644 --- a/src/Extension/Generated/ClassPlan.php +++ b/src/Extension/Generated/ClassPlan.php @@ -7,7 +7,7 @@ use Patchlevel\Hydrator\Metadata\ClassMetadata; /** - * A class handled by the generated middleware together with the plans of its properties. + * A class handled by the generated transformer together with the plans of its properties. * * @internal */ diff --git a/src/Extension/Generated/ClassPlanner.php b/src/Extension/Generated/ClassPlanner.php index a262bd34..d1995b67 100644 --- a/src/Extension/Generated/ClassPlanner.php +++ b/src/Extension/Generated/ClassPlanner.php @@ -22,15 +22,15 @@ use const PHP_VERSION_ID; /** - * Decides which classes end up in one generated middleware and how each of their properties is handled. + * Decides which classes end up in one generated transformer and how each of their properties is handled. * - * The planner accumulates the state of a single dump, create a new instance for every middleware. + * The planner accumulates the state of a single transformer, create a new instance for every transformer. * * @internal */ final class ClassPlanner { - /** @var array class => index, null for classes which can not be part of the middleware */ + /** @var array class => index, null for classes which can not be inlined */ private array $index = []; /** @var array */ @@ -67,7 +67,7 @@ public function add(string $class): void } if (!self::generatable($metadata)) { - throw new ClassNotGeneratable($class, 'the class is internal, abstract or an interface'); + throw new ClassNotGeneratable($class, 'the class is internal, abstract, anonymous or an interface'); } $this->register($metadata); @@ -190,7 +190,7 @@ private function nestedClass(Normalizer $normalizer): int|null } /** - * Nested classes can only be inlined if they would end up in this middleware anyway. + * Nested classes can only be inlined if code can be generated for them. * * @param class-string $class */ @@ -244,12 +244,22 @@ private static function scoped(ClassMetadata $metadata, array $properties): arra return [$hydrate, $extract]; } - /** Constructor visibility does not matter, the generated code runs in the scope of the class. */ - private static function generatable(ClassMetadata $metadata): bool + /** + * Constructor visibility does not matter, the generated code runs in the scope of the class. + * + * @param ClassMetadata $metadata + * + * @template T of object + */ + public static function generatable(ClassMetadata $metadata): bool { $reflection = $metadata->reflection; - return !$reflection->isInternal() && !$reflection->isAbstract() && !$reflection->isInterface() && !$reflection->isEnum(); + return !$reflection->isInternal() + && !$reflection->isAbstract() + && !$reflection->isInterface() + && !$reflection->isEnum() + && !$reflection->isAnonymous(); } /** Readonly and asymmetric visibility restrict writes to the class scope. */ diff --git a/src/Extension/Generated/CodeEmitter.php b/src/Extension/Generated/CodeEmitter.php index 0f5c4541..0cf3a00d 100644 --- a/src/Extension/Generated/CodeEmitter.php +++ b/src/Extension/Generated/CodeEmitter.php @@ -5,7 +5,6 @@ namespace Patchlevel\Hydrator\Extension\Generated; use function array_filter; -use function array_keys; use function array_unshift; use function assert; use function implode; @@ -14,14 +13,14 @@ use function var_export; /** - * Turns the plans of a middleware into php code. + * Turns the plans of a transformer into php code. * - * The generated middleware behaves like the {@see \Patchlevel\Hydrator\Middleware\TransformMiddleware}: + * The generated transformer behaves like the {@see \Patchlevel\Hydrator\Transformer\ReflectionTransformer}: * objects are instantiated without their constructor and the properties are assigned directly. Assigning * properties happens inside closures bound to the scope of the class, so private and readonly properties * work the same way as with reflection. * - * The emitter accumulates the members of a single generated class, create a new instance for every middleware. + * The emitter accumulates the members of a single generated class, create a new instance for every transformer. * * @internal */ @@ -44,17 +43,12 @@ public function __construct( ) { } + /** The first plan is the class of the transformer, the others are nested classes which can be inlined. */ public function emit(string $namespace, string $className): string { $constructor = []; - $compiledHydrateCases = []; - $compiledExtractCases = []; - $skipCases = []; - $hydrateCases = []; - $extractCases = []; foreach ($this->classes as $plan) { - $class = $plan->className(); $index = $plan->index; // until the class is initialized, the closures point to a stub which initializes the class first @@ -67,34 +61,20 @@ public function emit(string $namespace, string $className): string $index, ); - $hydrateCall = $plan->scopedHydrate ? sprintf('($this->h%d)', $index) : sprintf('$this->hydrate%d', $index); - $extractCall = $plan->scopedExtract ? sprintf('$this->e%d', $index) : sprintf('$this->extract%d(...)', $index); - $compiledHydrateCases[] = Templates::render(Templates::COMPILED_CASE, ['class' => $class, 'index' => (string)$index, 'closure' => sprintf('$this->fastHydrate%d(...)', $index)]); - $compiledExtractCases[] = Templates::render(Templates::COMPILED_CASE, ['class' => $class, 'index' => (string)$index, 'closure' => $extractCall]); - $this->methods[] = Templates::render(Templates::FAST_HYDRATE, ['index' => (string)$index, 'call' => $hydrateCall]); - - $skipCases[] = sprintf('\\%s::class,', $class); - $hydrateCases[] = $plan->scopedHydrate - ? sprintf('\\%s::class => ($this->h%d)($data, $populate, $object),', $class, $index) - : sprintf('\\%1$s::class => $this->ready%2$d ? $this->hydrate%2$d($data, $populate, $object) : ($this->h%2$d)($data, $populate, $object),', $class, $index); - $extractCases[] = $plan->scopedExtract - ? sprintf('\\%s::class => ($this->e%d)($object, $context),', $class, $index) - : sprintf('\\%1$s::class => $this->ready%2$d ? $this->extract%2$d($object, $context) : ($this->e%2$d)($object, $context),', $class, $index); - $this->emitClass($plan); } - return Templates::render(Templates::MIDDLEWARE, [ + $root = $this->classes[0]; + + return Templates::render(Templates::TRANSFORMER, [ 'namespace' => $namespace, 'className' => $className, - 'version' => (string)MiddlewareGenerator::VERSION, + 'class' => $root->className(), + 'version' => (string)TransformerGenerator::VERSION, 'properties' => implode("\n", $this->properties), 'constructor' => implode("\n", $constructor), - 'skipCases' => implode("\n", $skipCases), - 'compiledHydrateCases' => implode("\n", $compiledHydrateCases), - 'compiledExtractCases' => implode("\n", $compiledExtractCases), - 'hydrateCases' => implode("\n", $hydrateCases), - 'extractCases' => implode("\n", $extractCases), + 'hydrateCall' => $this->nestedHydrateCall($root), + 'extractCall' => $this->nestedExtractCall($root), 'inits' => implode("\n\n", $this->inits), 'methods' => implode("\n\n", $this->methods), ]); @@ -109,10 +89,11 @@ private function emitClass(ClassPlan $plan): void $this->declareProperties($plan); $init = []; - $nestedClasses = []; - $init[] = sprintf('$metadata = $this->metadata(\\%s::class, %s);', $class, var_export(ClassFingerprint::of($plan->metadata), true)); + // the fingerprint of the class is part of the file name and nested classes are checked before they are inlined + $init[] = sprintf('$metadata = $this->hydrator->metadata(\\%s::class);', $class); $init[] = sprintf('$this->r%d = $metadata->reflection;', $index); + $nestedInits = []; foreach ($plan->properties as $property) { if ($property->defaultSlot !== null) { @@ -129,18 +110,19 @@ private function emitClass(ClassPlan $plan): void continue; } - $nested = $this->nested($property)->className(); - $nestedClasses[$property->nested] = true; + $nested = $this->nested($property); + $fingerprint = var_export(ClassFingerprint::of($nested->metadata), true); $check = $property->kind === ValueKind::NestedObject ? 'inlineObject' : 'inlineArray'; - $init[] = sprintf('$this->ih%d = $this->%s($this->n%d, \\%s::class, Skip::Hydrate);', $property->flag, $check, $property->slot, $nested); - $init[] = sprintf('$this->ie%d = $this->%s($this->n%d, \\%s::class, Skip::Extract);', $property->flag, $check, $property->slot, $nested); + $init[] = sprintf('$this->ih%d = $this->%s($this->n%d, \\%s::class, %s, Skip::Hydrate);', $property->flag, $check, $property->slot, $nested->className(), $fingerprint); + $init[] = sprintf('$this->ie%d = $this->%s($this->n%d, \\%s::class, %s, Skip::Extract);', $property->flag, $check, $property->slot, $nested->className(), $fingerprint); - if ($property->kind !== ValueKind::NestedArray) { - continue; + if ($property->kind === ValueKind::NestedArray) { + $init[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $property->flag, $property->inner, $property->slot); } - $init[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $property->flag, $property->inner, $property->slot); + // the nested code is only initialized if it is inlined, outdated code is never touched + $nestedInits[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->init%2$d(); }', $property->flag, $nested->index); } [$hydrate, $hydrateHelpers] = $this->hydrateBody($plan); @@ -158,8 +140,8 @@ private function emitClass(ClassPlan $plan): void $init[] = $this->wrap($plan, 'e', 'extract', 'object $object, array $context', 'array', $extract, $plan->scopedExtract); - foreach (array_keys($nestedClasses) as $nested) { - $init[] = sprintf('$this->init%d();', $nested); + foreach ($nestedInits as $nestedInit) { + $init[] = $nestedInit; } if (!$plan->leaf()) { @@ -340,7 +322,7 @@ private function denormalize(ClassPlan $plan, PropertyPlan $property, string $va 'field' => $field, 'flag' => (string)$property->flag, 'slot' => (string)$property->slot, - 'nested' => $this->nestedHydrateCall($property), + 'nested' => $this->nestedHydrateCall($this->nested($property)), ]), ValueKind::NestedArray => Templates::render(Templates::DENORMALIZE_ARRAY, [ 'variable' => $variable, @@ -348,7 +330,7 @@ private function denormalize(ClassPlan $plan, PropertyPlan $property, string $va 'flag' => (string)$property->flag, 'slot' => (string)$property->slot, 'inner' => (string)$property->inner, - 'nested' => $this->nestedHydrateCall($property), + 'nested' => $this->nestedHydrateCall($this->nested($property)), ]), }; @@ -454,7 +436,7 @@ private function normalize(ClassPlan $plan, PropertyPlan $property): array 'name' => $name, 'flag' => (string)$property->flag, 'slot' => (string)$property->slot, - 'nested' => $this->nestedExtractCall($property), + 'nested' => $this->nestedExtractCall($this->nested($property)), 'check' => $this->instanceCheck('$value', $property), ]), ValueKind::NestedArray => Templates::render(Templates::NORMALIZE_ARRAY, [ @@ -463,7 +445,7 @@ private function normalize(ClassPlan $plan, PropertyPlan $property): array 'flag' => (string)$property->flag, 'slot' => (string)$property->slot, 'inner' => (string)$property->inner, - 'nested' => $this->nestedExtractCall($property), + 'nested' => $this->nestedExtractCall($this->nested($property)), 'check' => $this->instanceCheck('$item', $property), ]), }; @@ -491,17 +473,13 @@ private function instanceCheck(string $variable, PropertyPlan $property): string return sprintf('%1$s instanceof \\%2$s && %1$s::class === \\%2$s::class', $variable, $class); } - private function nestedHydrateCall(PropertyPlan $property): string + private function nestedHydrateCall(ClassPlan $nested): string { - $nested = $this->nested($property); - return $nested->scopedHydrate ? sprintf('($this->h%d)', $nested->index) : sprintf('$this->hydrate%d', $nested->index); } - private function nestedExtractCall(PropertyPlan $property): string + private function nestedExtractCall(ClassPlan $nested): string { - $nested = $this->nested($property); - return $nested->scopedExtract ? sprintf('($this->e%d)', $nested->index) : sprintf('$this->extract%d', $nested->index); } diff --git a/src/Extension/Generated/GeneratedCode.php b/src/Extension/Generated/GeneratedCode.php new file mode 100644 index 00000000..081835af --- /dev/null +++ b/src/Extension/Generated/GeneratedCode.php @@ -0,0 +1,19 @@ + $classes the class of the transformer first, followed by the inlined nested classes + */ + public function __construct( + public readonly string $code, + public readonly array $classes, + ) { + } +} diff --git a/src/Extension/Generated/GeneratedDecorator.php b/src/Extension/Generated/GeneratedDecorator.php deleted file mode 100644 index 3183be26..00000000 --- a/src/Extension/Generated/GeneratedDecorator.php +++ /dev/null @@ -1,36 +0,0 @@ -load)(new StackMetadataFactory($stack)); - - $this->slot->load($middleware); - - if ($middleware instanceof HydratorAwareMiddleware) { - $middleware->setHydrator($stack); - } - - return new GeneratedHydrator($hydrator, $stack, $middleware); - } -} diff --git a/src/Extension/Generated/GeneratedHydrator.php b/src/Extension/Generated/GeneratedHydrator.php deleted file mode 100644 index edc8a580..00000000 --- a/src/Extension/Generated/GeneratedHydrator.php +++ /dev/null @@ -1,114 +0,0 @@ - false if the class has no compiled hydrator */ - private array $hydrators = []; - - /** @var array false if the class has no compiled extractor */ - private array $extractors = []; - - public function __construct( - private readonly Hydrator $hydrator, - private readonly StackHydrator $stack, - private readonly GeneratedMiddleware $middleware, - ) { - } - - /** - * @param class-string $class - * @param array $context - * - * @return T - * - * @template T of object - */ - public function hydrate(string $class, mixed $data, array $context = []): object - { - $hydrator = $this->hydrators[$class] ?? $this->compileHydrator($class); - - if ($hydrator !== false && is_array($data)) { - $object = $hydrator($data, $context); - assert($object instanceof $class); - - return $object; - } - - return $this->hydrator->hydrate($class, $data, $context); - } - - /** @param array $context */ - public function extract(object $object, array $context = []): mixed - { - $extractor = $this->extractors[$object::class] ?? $this->compileExtractor($object::class); - - if ($extractor !== false) { - return $extractor($object, $context); - } - - return $this->hydrator->extract($object, $context); - } - - /** @internal */ - public function stack(): StackHydrator - { - return $this->stack; - } - - /** @internal */ - public function middleware(): GeneratedMiddleware - { - return $this->middleware; - } - - /** - * Decided once per class: the class must not use a class normalizer or a lazy proxy, and the generated - * middleware must handle it exclusively. Everything else is left to the stack. - * - * @param class-string $class - */ - private function compileHydrator(string $class): Closure|false - { - try { - $metadata = $this->stack->metadata($class); - } catch (ClassNotFound) { - return $this->hydrators[$class] = false; - } - - if ($metadata->normalizer !== null || (PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->stack->defaultLazy()))) { - return $this->hydrators[$class] = false; - } - - return $this->hydrators[$class] = $this->middleware->compiledHydrator($metadata) ?? false; - } - - /** @param class-string $class */ - private function compileExtractor(string $class): Closure|false - { - $metadata = $this->stack->metadata($class); - - if ($metadata->normalizer !== null) { - return $this->extractors[$class] = false; - } - - return $this->extractors[$class] = $this->middleware->compiledExtractor($metadata) ?? false; - } -} diff --git a/src/Extension/Generated/GeneratedMiddleware.php b/src/Extension/Generated/GeneratedMiddleware.php deleted file mode 100644 index 700845bb..00000000 --- a/src/Extension/Generated/GeneratedMiddleware.php +++ /dev/null @@ -1,36 +0,0 @@ - $metadata - * - * @return (Closure(array, array): object)|null null if the class is not handled exclusively by this middleware - * - * @template T of object - */ - public function compiledHydrator(ClassMetadata $metadata): Closure|null; - - /** - * @param ClassMetadata $metadata - * - * @return (Closure(object, array): array)|null null if the class is not handled exclusively by this middleware - * - * @template T of object - */ - public function compiledExtractor(ClassMetadata $metadata): Closure|null; -} diff --git a/src/Extension/Generated/GeneratedMiddlewareExtension.php b/src/Extension/Generated/GeneratedMiddlewareExtension.php deleted file mode 100644 index 668fe817..00000000 --- a/src/Extension/Generated/GeneratedMiddlewareExtension.php +++ /dev/null @@ -1,108 +0,0 @@ - $classes Classes for which code is generated, nested classes are discovered automatically. - * @param bool $debug Regenerate the middleware on every build. - * @param string|null $className Name of the generated class, derived from the configuration by default. - */ - public function __construct( - private readonly string $cachePath, - private readonly array $classes, - private readonly bool $debug = false, - private readonly string|null $className = null, - ) { - } - - /** - * The generated code is loaded when the hydrator is built, not here: extensions registered later may still add - * guessers and enrichers, and the generated code has to match the metadata the hydrator ends up with. - * The decorator has the lowest priority, so it is the innermost and all other decorators wrap it. - */ - public function configure(StackHydratorBuilder $builder): void - { - $slot = new GeneratedMiddlewareSlot(); - - $builder->addMiddleware($slot, Extension::PRIORITY_TRANSFORM + 1); - $builder->addDecorator(new GeneratedDecorator($slot, $this->middleware(...)), -32); - } - - private function middleware(MetadataFactory $metadataFactory): GeneratedMiddleware - { - $className = $this->className ?? $this->defaultClassName(); - $fqcn = self::NAMESPACE . '\\' . $className; - - if (!class_exists($fqcn, false)) { - $this->load($fqcn, $className, $metadataFactory); - } - - $middleware = new $fqcn(); - assert($middleware instanceof GeneratedMiddleware); - - return $middleware; - } - - public function defaultClassName(): string - { - return 'TransformMiddleware_' . substr( - hash('sha256', serialize([MiddlewareGenerator::VERSION, $this->classes])), - 0, - 16, - ); - } - - private function load(string $fqcn, string $className, MetadataFactory $metadataFactory): void - { - $file = sprintf('%s/%s.php', $this->cachePath, $className); - - if ($this->debug || !file_exists($file)) { - $generator = new MiddlewareGenerator($metadataFactory); - $this->write($file, $generator->dump($this->classes, $fqcn)); - } - - require $file; - } - - private function write(string $file, string $code): void - { - if (!is_dir($this->cachePath) && !@mkdir($this->cachePath, 0777, true) && !is_dir($this->cachePath)) { - throw new GeneratedMiddlewareNotWritable($file); - } - - $tmp = $file . '.' . uniqid('', true) . '.tmp'; - - if (@file_put_contents($tmp, $code) === false || !@rename($tmp, $file)) { - @unlink($tmp); - - throw new GeneratedMiddlewareNotWritable($file); - } - } -} diff --git a/src/Extension/Generated/GeneratedMiddlewareNotLoaded.php b/src/Extension/Generated/GeneratedMiddlewareNotLoaded.php deleted file mode 100644 index 53b229ea..00000000 --- a/src/Extension/Generated/GeneratedMiddlewareNotLoaded.php +++ /dev/null @@ -1,16 +0,0 @@ -middleware = $middleware; - } - - public function holds(GeneratedMiddleware $middleware): bool - { - return $this->middleware === $middleware; - } - - public function skip(ClassMetadata $metadata): Skip - { - $middleware = $this->middleware(); - - return $middleware instanceof SkippableMiddleware ? $middleware->skip($metadata) : Skip::None; - } - - /** - * @param ClassMetadata $metadata - * @param array $data - * @param array $context - * - * @return T - * - * @template T of object - */ - public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object - { - return $this->middleware()->hydrate($metadata, $data, $context, $stack); - } - - /** - * @param ClassMetadata $metadata - * @param T $object - * @param array $context - * - * @return array - * - * @template T of object - */ - public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array - { - return $this->middleware()->extract($metadata, $object, $context, $stack); - } - - private function middleware(): GeneratedMiddleware - { - return $this->middleware ?? throw new GeneratedMiddlewareNotLoaded(); - } -} diff --git a/src/Extension/Generated/GeneratedTransformerExtension.php b/src/Extension/Generated/GeneratedTransformerExtension.php new file mode 100644 index 00000000..152689f1 --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformerExtension.php @@ -0,0 +1,34 @@ +addTransformerFactory(new GeneratedTransformerFactory( + new TransformerFiles($this->cachePath), + $this->autoGenerate, + )); + } +} diff --git a/src/Extension/Generated/GeneratedTransformerFactory.php b/src/Extension/Generated/GeneratedTransformerFactory.php new file mode 100644 index 00000000..c0b7a233 --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformerFactory.php @@ -0,0 +1,71 @@ +files->className($metadata, $metadataFactory); + $fqcn = $this->files->fqcn($className); + + if (!class_exists($fqcn, false)) { + $file = $this->files->path($className); + + if (!is_file($file)) { + if (!$this->autoGenerate) { + return null; + } + + $code = (new TransformerGenerator($metadataFactory))->generate($metadata->className, $fqcn); + + if ($code === null) { + return null; + } + + $this->files->write($className, $code->code); + } + + require_once $file; + } + + try { + $transformer = new $fqcn($hydrator); + } catch (OutdatedGeneratedTransformer) { + return null; + } + + assert($transformer instanceof ClassTransformer); + + return $transformer; + } +} diff --git a/src/Extension/Generated/GeneratedMiddlewareNotWritable.php b/src/Extension/Generated/GeneratedTransformerNotWritable.php similarity index 53% rename from src/Extension/Generated/GeneratedMiddlewareNotWritable.php rename to src/Extension/Generated/GeneratedTransformerNotWritable.php index 8cd52fc7..4208469b 100644 --- a/src/Extension/Generated/GeneratedMiddlewareNotWritable.php +++ b/src/Extension/Generated/GeneratedTransformerNotWritable.php @@ -9,10 +9,10 @@ use function sprintf; -final class GeneratedMiddlewareNotWritable extends RuntimeException implements HydratorException +final class GeneratedTransformerNotWritable extends RuntimeException implements HydratorException { public function __construct(string $file) { - parent::__construct(sprintf('The generated middleware could not be written to "%s".', $file)); + parent::__construct(sprintf('The generated transformer could not be written to "%s".', $file)); } } diff --git a/src/Extension/Generated/HydratorNotSet.php b/src/Extension/Generated/HydratorNotSet.php deleted file mode 100644 index 990f2f7a..00000000 --- a/src/Extension/Generated/HydratorNotSet.php +++ /dev/null @@ -1,18 +0,0 @@ - $classes - * - * @throws ClassNotGeneratable - */ - public function dump(array $classes, string $middlewareFqcn): string - { - $planner = new ClassPlanner($this->metadataFactory); - - foreach ($classes as $class) { - $planner->add($class); - } - - $parts = explode('\\', ltrim($middlewareFqcn, '\\')); - $className = array_pop($parts); - $namespace = implode('\\', $parts); - - return (new CodeEmitter($planner->plans()))->emit($namespace, $className); - } -} diff --git a/src/Extension/Generated/OutdatedGeneratedMiddleware.php b/src/Extension/Generated/OutdatedGeneratedMiddleware.php deleted file mode 100644 index 2f101130..00000000 --- a/src/Extension/Generated/OutdatedGeneratedMiddleware.php +++ /dev/null @@ -1,21 +0,0 @@ - */ + public array $middlewares; - /** @var list all middlewares except this one, its slot and the TransformMiddleware behind it */ - public array $others = []; - - /** @var array */ - public array $callStack = []; + public CallStack $callStack; {{properties}} - public function __construct() + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) { - {{constructor}} - } - - public function setHydrator(StackHydrator $hydrator): void - { - $this->hydrator = $hydrator; - $this->others = []; - - foreach ($hydrator->middlewares() as $middleware) { - if ($middleware === $this || $middleware instanceof TransformMiddleware) { - continue; - } + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); - if ($middleware instanceof GeneratedMiddlewareSlot && $middleware->holds($this)) { - continue; - } - - $this->others[] = $middleware; - } - } - - /** Lets the GeneratedHydrator call the generated code directly, skipping the middleware stack. */ - public function compiledHydrator(ClassMetadata $metadata): Closure|null - { - if (!$this->exclusive($metadata, Skip::Hydrate)) { - return null; - } - - switch ($metadata->className) { - {{compiledHydrateCases}} - default: - return null; - } - } - - public function compiledExtractor(ClassMetadata $metadata): Closure|null - { - if (!$this->exclusive($metadata, Skip::Extract)) { - return null; - } - - switch ($metadata->className) { - {{compiledExtractCases}} - default: - return null; - } - } + {{constructor}} - /** Classes without generated code are left to the TransformMiddleware directly. */ - public function skip(ClassMetadata $metadata): Skip - { - return match ($metadata->className) { - {{skipCases}} - => Skip::None, - default => Skip::Both, - }; + $this->init0(); } - public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + public function hydrate(array $data, array $context): object { - $object = null; - $populate = $context; - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($populate[Hydrator::OBJECT_TO_POPULATE]); + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return {{hydrateCall}}($data, $context, $object); } - return match ($metadata->className) { - {{hydrateCases}} - default => $stack->next()->hydrate($metadata, $data, $context, $stack), - }; + return {{hydrateCall}}($data, $context); } - public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + public function extract(object $object, array $context): array { - return match ($metadata->className) { - {{extractCases}} - default => $stack->next()->extract($metadata, $object, $context, $stack), - }; + return {{extractCall}}($object, $context); } {{inits}} {{methods}} - private function hydrator(): StackHydrator - { - return $this->hydrator ?? throw new HydratorNotSet(self::class); - } - - /** @param string $fingerprint the {@see ClassFingerprint} of the class when the code was generated */ - private function metadata(string $class, string $fingerprint): ClassMetadata - { - $metadata = $this->hydrator()->metadata($class); - - if (ClassFingerprint::of($metadata) !== $fingerprint) { - throw new OutdatedGeneratedMiddleware($class); - } - - return $metadata; - } - private function normalizer(ClassMetadata $metadata, string $property): Normalizer { - return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedMiddleware($metadata->className); + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); } private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter { - return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedMiddleware($metadata->className); + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); } - private function inlineObject(Normalizer $normalizer, string $class, Skip $direction): bool + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool { if (!$normalizer instanceof ObjectNormalizer) { return false; @@ -237,48 +157,41 @@ private function inlineObject(Normalizer $normalizer, string $class, Skip $direc return false; } - return $this->inlinable($class, $direction); + return $this->inlinable($class, $fingerprint, $direction); } - private function inlineArray(Normalizer $normalizer, string $class, Skip $direction): bool + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool { - return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $direction); + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); } /** - * Nested objects are only inlined when this does not change the behaviour: the nested class must be handled - * by this middleware, must not be lazy and every other middleware must skip the class in this direction. + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. */ - private function inlinable(string $class, Skip $direction): bool + private function inlinable(string $class, string $fingerprint, Skip $direction): bool { // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator - $root = $this->hydrator()->rootHydrator(); - - if ($root !== $this->hydrator() && !$root instanceof GeneratedHydrator) { + if ($this->hydrator->rootHydrator() !== $this->hydrator) { return false; } try { - $metadata = $this->hydrator()->metadata($class); + $metadata = $this->hydrator->metadata($class); } catch (Throwable) { return false; } - if ($metadata->normalizer !== null) { + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { return false; } - if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator()->defaultLazy())) { + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { return false; } - return $this->exclusive($metadata, $direction); - } - - /** Whether every other middleware skips the class in this direction, so this middleware is the only one to run. */ - private function exclusive(ClassMetadata $metadata, Skip $direction): bool - { - foreach ($this->others as $middleware) { + foreach ($this->middlewares as $middleware) { if (!$middleware instanceof SkippableMiddleware) { return false; } @@ -294,27 +207,6 @@ private function exclusive(ClassMetadata $metadata, Skip $direction): bool } } -PHP; - - public const COMPILED_CASE = <<<'PHP' -case \{{class}}::class: - $this->init{{index}}(); - - return {{closure}}; -PHP; - - public const FAST_HYDRATE = <<<'PHP' -public function fastHydrate{{index}}(array $data, array $context): object -{ - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return {{call}}($data, $context, $object); - } - - return {{call}}($data, $context); -} PHP; public const INIT = <<<'PHP' @@ -361,14 +253,14 @@ public function {{name}}({{signature}}): {{return}} if ($this->tracked{{index}}) { $id = \spl_object_id($object); - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); $references[] = $object::class; throw new CircularReference($references); } - $this->callStack[$id] = \{{class}}::class; + $this->callStack->objects[$id] = \{{class}}::class; try { {{pre}} @@ -377,7 +269,7 @@ public function {{name}}({{signature}}): {{return}} {{fields}} ]; } finally { - unset($this->callStack[$id]); + unset($this->callStack->objects[$id]); } } PHP; diff --git a/src/Extension/Generated/TransformerCompiler.php b/src/Extension/Generated/TransformerCompiler.php new file mode 100644 index 00000000..b17fdf24 --- /dev/null +++ b/src/Extension/Generated/TransformerCompiler.php @@ -0,0 +1,81 @@ +metadataFactory(), $cachePath))->compile([ProfileCreated::class]); + */ +final class TransformerCompiler +{ + private readonly TransformerFiles $files; + + public function __construct( + private readonly MetadataFactory $metadataFactory, + string $cachePath, + ) { + $this->files = new TransformerFiles($cachePath); + } + + /** + * Generates a transformer for every class and every nested class it references. + * + * @param list $classes + * + * @return list all classes a transformer was generated for + * + * @throws ClassNotGeneratable + * @throws GeneratedTransformerNotWritable + */ + public function compile(array $classes): array + { + $generator = new TransformerGenerator($this->metadataFactory); + $queue = $classes; + $done = []; + $generated = []; + + while ($queue !== []) { + /** @var class-string $class */ + $class = ltrim(array_shift($queue), '\\'); + + if (isset($done[$class])) { + continue; + } + + $done[$class] = true; + + try { + $metadata = $this->metadataFactory->metadata($class); + } catch (ClassNotFound) { + throw new ClassNotGeneratable($class, 'class not found'); + } + + $className = $this->files->className($metadata, $this->metadataFactory); + $code = $generator->generate($class, $this->files->fqcn($className)); + + if ($code === null) { + continue; + } + + $this->files->write($className, $code->code); + $generated[] = $class; + + // nested classes are also hydrated on their own if they can not be inlined + foreach ($code->classes as $nested) { + $queue[] = $nested; + } + } + + return $generated; + } +} diff --git a/src/Extension/Generated/TransformerFiles.php b/src/Extension/Generated/TransformerFiles.php new file mode 100644 index 00000000..aafc3356 --- /dev/null +++ b/src/Extension/Generated/TransformerFiles.php @@ -0,0 +1,88 @@ + $metadata + * + * @template T of object + */ + public function className(ClassMetadata $metadata, MetadataFactory $metadataFactory): string + { + $hash = hash('sha256', serialize([ + TransformerGenerator::VERSION, + $metadata->className, + ClassFingerprint::ofGraph($metadata, $metadataFactory), + ])); + + return sprintf( + '%sTransformer_%s', + preg_replace('/[^A-Za-z0-9_]/', '', $metadata->reflection->getShortName()), + substr($hash, 0, 16), + ); + } + + /** @return class-string */ + public function fqcn(string $className): string + { + /** @var class-string $fqcn */ + $fqcn = self::NAMESPACE . '\\' . $className; + + return $fqcn; + } + + public function path(string $className): string + { + return sprintf('%s/%s.php', $this->cachePath, $className); + } + + /** @throws GeneratedTransformerNotWritable */ + public function write(string $className, string $code): void + { + $file = $this->path($className); + + if (!is_dir($this->cachePath) && !@mkdir($this->cachePath, 0777, true) && !is_dir($this->cachePath)) { + throw new GeneratedTransformerNotWritable($file); + } + + $tmp = $file . '.' . uniqid('', true) . '.tmp'; + + if (@file_put_contents($tmp, $code) === false || !@rename($tmp, $file)) { + @unlink($tmp); + + throw new GeneratedTransformerNotWritable($file); + } + } +} diff --git a/src/Extension/Generated/TransformerGenerator.php b/src/Extension/Generated/TransformerGenerator.php new file mode 100644 index 00000000..81437380 --- /dev/null +++ b/src/Extension/Generated/TransformerGenerator.php @@ -0,0 +1,61 @@ +metadataFactory); + $planner->add($class); + + $plans = $planner->plans(); + + if ($plans === []) { + return null; + } + + $parts = explode('\\', ltrim($transformerFqcn, '\\')); + $className = array_pop($parts); + $namespace = implode('\\', $parts); + + return new GeneratedCode( + (new CodeEmitter($plans))->emit($namespace, $className), + array_map(static fn (ClassPlan $plan): string => $plan->className(), $plans), + ); + } +} diff --git a/src/Extension/Generated/ValueKind.php b/src/Extension/Generated/ValueKind.php index e7a8072b..adb76646 100644 --- a/src/Extension/Generated/ValueKind.php +++ b/src/Extension/Generated/ValueKind.php @@ -17,7 +17,7 @@ enum ValueKind /** A normalizer which is called at runtime. */ case Normalizer; - /** An object normalizer for a class of the same middleware, the nested code can be called directly. */ + /** An object normalizer for a class of the same transformer, the nested code can be called directly. */ case NestedObject; /** An array normalizer of nested objects, the nested code can be called directly for each item. */ diff --git a/src/Extension/Tracing/TracingDecorator.php b/src/Extension/Tracing/TracingDecorator.php index 69a18998..3c42e11c 100644 --- a/src/Extension/Tracing/TracingDecorator.php +++ b/src/Extension/Tracing/TracingDecorator.php @@ -6,7 +6,6 @@ use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\HydratorDecorator; -use Patchlevel\Hydrator\StackHydrator; final class TracingDecorator implements HydratorDecorator { @@ -15,7 +14,7 @@ public function __construct( ) { } - public function decorate(Hydrator $hydrator, StackHydrator $stack): Hydrator + public function decorate(Hydrator $hydrator): Hydrator { return new TracingHydrator($hydrator, $this->tracer); } diff --git a/src/HydratorDecorator.php b/src/HydratorDecorator.php index f4390488..353f53a4 100644 --- a/src/HydratorDecorator.php +++ b/src/HydratorDecorator.php @@ -9,9 +9,6 @@ */ interface HydratorDecorator { - /** - * @param Hydrator $hydrator the hydrator to wrap, either the stack hydrator or an already decorated one - * @param StackHydrator $stack the innermost stack hydrator, for decorators which need its metadata - */ - public function decorate(Hydrator $hydrator, StackHydrator $stack): Hydrator; + /** @param Hydrator $hydrator the hydrator to wrap, either the stack hydrator or an already decorated one */ + public function decorate(Hydrator $hydrator): Hydrator; } diff --git a/src/Middleware/AllMiddlewaresSkipped.php b/src/Middleware/AllMiddlewaresSkipped.php index 87bafab7..4a3f435f 100644 --- a/src/Middleware/AllMiddlewaresSkipped.php +++ b/src/Middleware/AllMiddlewaresSkipped.php @@ -9,6 +9,7 @@ use function sprintf; +/** @deprecated if all middlewares are skipped, the data is only transformed */ final class AllMiddlewaresSkipped extends RuntimeException implements HydratorException { /** @param class-string $className */ diff --git a/src/Middleware/TransformMiddleware.php b/src/Middleware/TransformMiddleware.php index afc5cffe..652d2bc3 100644 --- a/src/Middleware/TransformMiddleware.php +++ b/src/Middleware/TransformMiddleware.php @@ -4,23 +4,37 @@ namespace Patchlevel\Hydrator\Middleware; -use Patchlevel\Hydrator\CircularReference; -use Patchlevel\Hydrator\DenormalizationFailure; -use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\ClassMetadata; -use Patchlevel\Hydrator\NormalizationFailure; -use Patchlevel\Hydrator\TypeMismatch; -use Throwable; -use TypeError; +use Patchlevel\Hydrator\StackHydrator; +use Patchlevel\Hydrator\Transformer\CallStack; +use Patchlevel\Hydrator\Transformer\ClassTransformer; +use Patchlevel\Hydrator\Transformer\ReflectionTransformer; + +use function assert; + +/** + * Turns the array into the object and back with the {@see ClassTransformer} which the hydrator provides for the class. + * The StackHydrator ends every stack with it, it does not have to be registered. Without a hydrator, for example in + * a test of a middleware, reflection is used. + */ +final class TransformMiddleware implements Middleware, HydratorAwareMiddleware +{ + private StackHydrator|null $hydrator = null; -use function array_key_exists; -use function array_values; -use function spl_object_id; + /** @var array only used without a hydrator */ + private array $transformers = []; -final class TransformMiddleware implements Middleware -{ - /** @var array */ - private array $callStack = []; + private readonly CallStack $callStack; + + public function __construct() + { + $this->callStack = new CallStack(); + } + + public function setHydrator(StackHydrator $hydrator): void + { + $this->hydrator = $hydrator; + } /** * @param ClassMetadata $metadata @@ -33,57 +47,8 @@ final class TransformMiddleware implements Middleware */ public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object { - $object = $context[Hydrator::OBJECT_TO_POPULATE] ?? $metadata->newInstance(); - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - $constructorParameters = null; - - foreach ($metadata->properties as $propertyMetadata) { - if (!array_key_exists($propertyMetadata->fieldName, $data)) { - if (!$propertyMetadata->reflection->isPromoted()) { - continue; - } - - $constructorParameters ??= $metadata->promotedConstructorDefaults(); - - if (!array_key_exists($propertyMetadata->propertyName, $constructorParameters)) { - continue; - } - - $propertyMetadata->setValue( - $object, - $constructorParameters[$propertyMetadata->propertyName]->getDefaultValue(), - ); - - continue; - } - - if ($propertyMetadata->normalizer) { - try { - /** @psalm-suppress MixedAssignment */ - $value = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure( - $metadata->className, - $propertyMetadata->propertyName, - $propertyMetadata->normalizer::class, - $e, - ); - } - } else { - $value = $data[$propertyMetadata->fieldName]; - } - - try { - $propertyMetadata->setValue($object, $value); - } catch (TypeError $e) { - throw new TypeMismatch( - $metadata->className, - $propertyMetadata->propertyName, - $e, - ); - } - } + $object = $this->transformer($metadata)->hydrate($data, $context); + assert($object instanceof $metadata->className); return $object; } @@ -95,46 +60,15 @@ public function hydrate(ClassMetadata $metadata, array $data, array $context, St */ public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array { - $objectId = spl_object_id($object); - - if (array_key_exists($objectId, $this->callStack)) { - $references = array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$objectId] = $object::class; - - try { - $data = []; + return $this->transformer($metadata)->extract($object, $context); + } - foreach ($metadata->properties as $propertyMetadata) { - if ($propertyMetadata->normalizer) { - try { - /** @psalm-suppress MixedAssignment */ - $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( - $propertyMetadata->getValue($object), - $context, - ); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure( - $object::class, - $propertyMetadata->propertyName, - $propertyMetadata->normalizer::class, - $e, - ); - } - } else { - $data[$propertyMetadata->fieldName] = $propertyMetadata->getValue($object); - } - } - } finally { - unset($this->callStack[$objectId]); + private function transformer(ClassMetadata $metadata): ClassTransformer + { + if ($this->hydrator !== null) { + return $this->hydrator->transformer($metadata); } - return $data; + return $this->transformers[$metadata->className] ??= new ReflectionTransformer($metadata, $this->callStack); } } diff --git a/src/MissingMiddlewares.php b/src/MissingMiddlewares.php index 817ac6b4..b338fc09 100644 --- a/src/MissingMiddlewares.php +++ b/src/MissingMiddlewares.php @@ -6,7 +6,7 @@ use RuntimeException; -/** @experimental */ +/** @deprecated a hydrator without middlewares is valid, it only transforms the data */ final class MissingMiddlewares extends RuntimeException implements HydratorException { public function __construct() diff --git a/src/StackHydrator.php b/src/StackHydrator.php index de5cad68..a65b3711 100644 --- a/src/StackHydrator.php +++ b/src/StackHydrator.php @@ -8,7 +8,6 @@ use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Metadata\ClassNotFound; use Patchlevel\Hydrator\Metadata\MetadataFactory; -use Patchlevel\Hydrator\Middleware\AllMiddlewaresSkipped; use Patchlevel\Hydrator\Middleware\HydratorAwareMiddleware; use Patchlevel\Hydrator\Middleware\Middleware; use Patchlevel\Hydrator\Middleware\Skip; @@ -16,9 +15,14 @@ use Patchlevel\Hydrator\Middleware\Stack; use Patchlevel\Hydrator\Middleware\TransformMiddleware; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; +use Patchlevel\Hydrator\Transformer\CallStack; +use Patchlevel\Hydrator\Transformer\ClassTransformer; +use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; +use Patchlevel\Hydrator\Transformer\ReflectionTransformer; use ReflectionClass; use function array_key_exists; +use function assert; use function is_array; use const PHP_VERSION_ID; @@ -28,41 +32,76 @@ final class StackHydrator implements Hydrator /** @var array */ private array $classMetadata = []; + /** @var list */ + private readonly array $middlewares; + + /** @var non-empty-list the middlewares followed by the transformation */ + private readonly array $pipeline; + + /** Ends every stack, it calls the transformer of the class. */ + private readonly TransformMiddleware $transform; + /** @var array> */ - private array $hydrateMiddlewares = []; + private array $hydratePipelines = []; /** @var array> */ - private array $extractMiddlewares = []; + private array $extractPipelines = []; + + /** @var array */ + private array $transformers = []; + + /** @var array classes which only need the transformation, so the stack is skipped */ + private array $directHydrators = []; + + /** @var array classes which only need the transformation, so the stack is skipped */ + private array $directExtractors = []; + + private readonly CallStack $callStack; private readonly bool $hasSkippableMiddlewares; /** The hydrator which is passed to the normalizers to hydrate and extract nested objects. */ private Hydrator $rootHydrator; - /** @param list $middlewares */ + /** + * The transformation always runs at the end, after all middlewares. A TransformMiddleware in the list is not + * needed anymore, it ends the list like before: middlewares after it are never called. + * + * @param list $middlewares + * @param ClassTransformerFactory|null $transformerFactory provides other transformers than reflection + */ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), - private readonly array $middlewares = [new TransformMiddleware()], + array $middlewares = [], private readonly bool $defaultLazy = false, + private readonly ClassTransformerFactory|null $transformerFactory = null, ) { - if ($middlewares === []) { - throw new MissingMiddlewares(); - } + $this->callStack = new CallStack(); + + $this->transform = new TransformMiddleware(); + $this->transform->setHydrator($this); + $list = []; $hasSkippableMiddlewares = false; foreach ($middlewares as $middleware) { + if ($middleware instanceof TransformMiddleware) { + break; + } + if ($middleware instanceof SkippableMiddleware) { $hasSkippableMiddlewares = true; } - if (!$middleware instanceof HydratorAwareMiddleware) { - continue; + if ($middleware instanceof HydratorAwareMiddleware) { + $middleware->setHydrator($this); } - $middleware->setHydrator($this); + $list[] = $middleware; } + $this->middlewares = $list; + $this->pipeline = [...$list, $this->transform]; $this->hasSkippableMiddlewares = $hasSkippableMiddlewares; $this->rootHydrator = $this; } @@ -91,7 +130,7 @@ public function rootHydrator(): Hydrator return $this->rootHydrator; } - /** @return list */ + /** @return list the middlewares, without the transformation at the end */ public function middlewares(): array { return $this->middlewares; @@ -112,6 +151,15 @@ public function defaultLazy(): bool */ public function hydrate(string $class, mixed $data, array $context = []): object { + $direct = $this->directHydrators[$class] ?? null; + + if ($direct !== null && is_array($data)) { + $object = $direct->hydrate($data, $context); + assert($object instanceof $class); + + return $object; + } + try { /** @var ClassMetadata $metadata */ $metadata = $this->classMetadata[$class] ?? $this->metadata($class); @@ -133,25 +181,35 @@ public function hydrate(string $class, mixed $data, array $context = []): object throw new ArrayDataRequired($class); } - if (PHP_VERSION_ID < 80400) { - $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); + if (PHP_VERSION_ID < 80400 || !($metadata->lazy ?? $this->defaultLazy)) { + $pipeline = $this->pipelineFor($metadata, Skip::Hydrate); - return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); - } + if (!isset($pipeline[1])) { + // no middleware runs for this class, from now on the transformer is called without the stack + $transformer = $this->directHydrators[$class] = $this->transformer($metadata); - $lazy = $metadata->lazy ?? $this->defaultLazy; + $object = $transformer->hydrate($data, $context); + assert($object instanceof $class); - if (!$lazy) { - $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); + return $object; + } - return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); + return $pipeline[0]->hydrate($metadata, $data, $context, new Stack($pipeline, 1)); } return (new ReflectionClass($class))->newLazyProxy( function () use ($metadata, $data, $context): object { - $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); + $pipeline = $this->pipelineFor($metadata, Skip::Hydrate); + + if (!isset($pipeline[1])) { + // not cached as direct hydrator, every call has to create a lazy proxy again + $object = $this->transformer($metadata)->hydrate($data, $context); + assert($object instanceof $metadata->className); + + return $object; + } - return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); + return $pipeline[0]->hydrate($metadata, $data, $context, new Stack($pipeline, 1)); }, ); } @@ -163,18 +221,59 @@ function () use ($metadata, $data, $context): object { */ public function extract(object $object, array $context = []): mixed { + $direct = $this->directExtractors[$object::class] ?? null; + + if ($direct !== null) { + return $direct->extract($object, $context); + } + $metadata = $this->classMetadata[$object::class] ?? $this->metadata($object::class); if ($metadata->normalizer) { return $metadata->normalizer->normalize($object, $context); } - $middlewares = $this->middlewaresFor($metadata, Skip::Extract); + $pipeline = $this->pipelineFor($metadata, Skip::Extract); + + if (!isset($pipeline[1])) { + // no middleware runs for this class, from now on the transformer is called without the stack + $transformer = $this->directExtractors[$object::class] = $this->transformer($metadata); + + return $transformer->extract($object, $context); + } + + return $pipeline[0]->extract($metadata, $object, $context, new Stack($pipeline, 1)); + } + + /** + * The transformer of the class, created once by the transformer factory or reflection based as fallback. + * + * @internal used by the {@see TransformMiddleware} at the end of the stack + * + * @param ClassMetadata $metadata + * + * @template T of object + */ + public function transformer(ClassMetadata $metadata): ClassTransformer + { + return $this->transformers[$metadata->className] + ??= $this->transformerFactory?->create($metadata, $this) + ?? new ReflectionTransformer($metadata, $this->callStack); + } - return $middlewares[0]->extract($metadata, $object, $context, new Stack($middlewares, 1)); + /** + * The objects which are currently extracted, transformers share it to detect circular references. + * + * @internal + */ + public function callStack(): CallStack + { + return $this->callStack; } /** + * The middlewares which run for the class in this direction, followed by the transformation. + * * @param ClassMetadata $metadata * @param Skip::Hydrate|Skip::Extract $direction * @@ -182,21 +281,21 @@ public function extract(object $object, array $context = []): mixed * * @template T of object */ - private function middlewaresFor(ClassMetadata $metadata, Skip $direction): array + private function pipelineFor(ClassMetadata $metadata, Skip $direction): array { if (!$this->hasSkippableMiddlewares) { - return $this->middlewares; + return $this->pipeline; } $cached = $direction === Skip::Hydrate - ? $this->hydrateMiddlewares[$metadata->className] ?? null - : $this->extractMiddlewares[$metadata->className] ?? null; + ? $this->hydratePipelines[$metadata->className] ?? null + : $this->extractPipelines[$metadata->className] ?? null; if ($cached !== null) { return $cached; } - $middlewares = []; + $pipeline = []; foreach ($this->middlewares as $middleware) { if ($middleware instanceof SkippableMiddleware) { @@ -207,18 +306,16 @@ private function middlewaresFor(ClassMetadata $metadata, Skip $direction): array } } - $middlewares[] = $middleware; + $pipeline[] = $middleware; } - if ($middlewares === []) { - throw new AllMiddlewaresSkipped($metadata->className); - } + $pipeline[] = $this->transform; if ($direction === Skip::Hydrate) { - return $this->hydrateMiddlewares[$metadata->className] = $middlewares; + return $this->hydratePipelines[$metadata->className] = $pipeline; } - return $this->extractMiddlewares[$metadata->className] = $middlewares; + return $this->extractPipelines[$metadata->className] = $pipeline; } /** diff --git a/src/StackHydratorBuilder.php b/src/StackHydratorBuilder.php index 42223032..2484e72c 100644 --- a/src/StackHydratorBuilder.php +++ b/src/StackHydratorBuilder.php @@ -9,9 +9,12 @@ use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\EnrichingMetadataFactory; use Patchlevel\Hydrator\Metadata\MetadataEnricher; +use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr16MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr6MetadataFactory; use Patchlevel\Hydrator\Middleware\Middleware; +use Patchlevel\Hydrator\Transformer\ChainTransformerFactory; +use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; use Psr\Cache\CacheItemPoolInterface; use Psr\SimpleCache\CacheInterface; @@ -35,6 +38,9 @@ final class StackHydratorBuilder /** @var array> */ private array $decorators = []; + /** @var array> */ + private array $transformerFactories = []; + private CacheItemPoolInterface|CacheInterface|null $cache = null; /** @return $this */ @@ -61,6 +67,18 @@ public function addGuesser(Guesser $guesser, int $priority = 0): static return $this; } + /** + * Factories with a higher priority are asked first, reflection is used if no factory provides a transformer. + * + * @return $this + */ + public function addTransformerFactory(ClassTransformerFactory $factory, int $priority = 0): static + { + $this->transformerFactories[$priority][] = $factory; + + return $this; + } + /** * Decorators with a higher priority are wrapped around the ones with a lower priority. * @@ -101,7 +119,7 @@ public function buildHydrator(): Hydrator $hydrator = $stack; foreach ($this->decorators() as $decorator) { - $hydrator = $decorator->decorate($hydrator, $stack); + $hydrator = $decorator->decorate($hydrator); } $stack->setRootHydrator($hydrator); @@ -125,7 +143,11 @@ public function build(): StackHydrator return $this->buildStackHydrator(); } - private function buildStackHydrator(): StackHydrator + /** + * The metadata factory with all registered guessers, enrichers and the cache, as the hydrator uses it. Useful to + * generate code ahead of time, see {@see \Patchlevel\Hydrator\Extension\Generated\TransformerCompiler}. + */ + public function metadataFactory(): MetadataFactory { $metadataFactory = new EnrichingMetadataFactory( new AttributeMetadataFactory( @@ -142,10 +164,18 @@ private function buildStackHydrator(): StackHydrator $metadataFactory = new Psr16MetadataFactory($metadataFactory, $this->cache); } + return $metadataFactory; + } + + private function buildStackHydrator(): StackHydrator + { + $transformerFactories = $this->transformerFactories(); + return new StackHydrator( - $metadataFactory, + $this->metadataFactory(), $this->middlewares(), $this->defaultLazy, + $transformerFactories === [] ? null : new ChainTransformerFactory($transformerFactories), ); } @@ -178,6 +208,14 @@ public function metadataEnrichers(): array return array_merge(...$this->metadataEnrichers); } + /** @return list */ + public function transformerFactories(): array + { + krsort($this->transformerFactories); + + return array_merge(...$this->transformerFactories); + } + /** @return list in the order they are applied, the innermost first */ public function decorators(): array { diff --git a/src/Transformer/CallStack.php b/src/Transformer/CallStack.php new file mode 100644 index 00000000..66f1a7a0 --- /dev/null +++ b/src/Transformer/CallStack.php @@ -0,0 +1,17 @@ + object id => class */ + public array $objects = []; +} diff --git a/src/Transformer/ChainTransformerFactory.php b/src/Transformer/ChainTransformerFactory.php new file mode 100644 index 00000000..bb53f58c --- /dev/null +++ b/src/Transformer/ChainTransformerFactory.php @@ -0,0 +1,30 @@ + $factories asked in this order, the first transformer wins */ + public function __construct( + private iterable $factories, + ) { + } + + public function create(ClassMetadata $metadata, StackHydrator $hydrator): ClassTransformer|null + { + foreach ($this->factories as $factory) { + $transformer = $factory->create($metadata, $hydrator); + + if ($transformer !== null) { + return $transformer; + } + } + + return null; + } +} diff --git a/src/Transformer/ClassTransformer.php b/src/Transformer/ClassTransformer.php new file mode 100644 index 00000000..4053c6ab --- /dev/null +++ b/src/Transformer/ClassTransformer.php @@ -0,0 +1,25 @@ + $data + * @param array $context may contain an object to populate, see {@see \Patchlevel\Hydrator\Hydrator::OBJECT_TO_POPULATE} + */ + public function hydrate(array $data, array $context): object; + + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context): array; +} diff --git a/src/Transformer/ClassTransformerFactory.php b/src/Transformer/ClassTransformerFactory.php new file mode 100644 index 00000000..c4e5bb31 --- /dev/null +++ b/src/Transformer/ClassTransformerFactory.php @@ -0,0 +1,25 @@ + $metadata + * + * @template T of object + */ + public function create(ClassMetadata $metadata, StackHydrator $hydrator): ClassTransformer|null; +} diff --git a/src/Transformer/ReflectionTransformer.php b/src/Transformer/ReflectionTransformer.php new file mode 100644 index 00000000..200371a0 --- /dev/null +++ b/src/Transformer/ReflectionTransformer.php @@ -0,0 +1,148 @@ + $metadata */ + public function __construct( + private readonly ClassMetadata $metadata, + private readonly CallStack $callStack = new CallStack(), + ) { + } + + /** + * @param array $data + * @param array $context + * + * @return T + */ + public function hydrate(array $data, array $context): object + { + $metadata = $this->metadata; + + $object = $context[Hydrator::OBJECT_TO_POPULATE] ?? $metadata->newInstance(); + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + $constructorParameters = null; + + foreach ($metadata->properties as $propertyMetadata) { + if (!array_key_exists($propertyMetadata->fieldName, $data)) { + if (!$propertyMetadata->reflection->isPromoted()) { + continue; + } + + $constructorParameters ??= $metadata->promotedConstructorDefaults(); + + if (!array_key_exists($propertyMetadata->propertyName, $constructorParameters)) { + continue; + } + + $propertyMetadata->setValue( + $object, + $constructorParameters[$propertyMetadata->propertyName]->getDefaultValue(), + ); + + continue; + } + + if ($propertyMetadata->normalizer) { + try { + /** @psalm-suppress MixedAssignment */ + $value = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure( + $metadata->className, + $propertyMetadata->propertyName, + $propertyMetadata->normalizer::class, + $e, + ); + } + } else { + $value = $data[$propertyMetadata->fieldName]; + } + + try { + $propertyMetadata->setValue($object, $value); + } catch (TypeError $e) { + throw new TypeMismatch( + $metadata->className, + $propertyMetadata->propertyName, + $e, + ); + } + } + + return $object; + } + + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context): array + { + $objectId = spl_object_id($object); + + if (array_key_exists($objectId, $this->callStack->objects)) { + $references = array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$objectId] = $object::class; + + try { + $data = []; + + foreach ($this->metadata->properties as $propertyMetadata) { + if ($propertyMetadata->normalizer) { + try { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + $context, + ); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure( + $object::class, + $propertyMetadata->propertyName, + $propertyMetadata->normalizer::class, + $e, + ); + } + } else { + $data[$propertyMetadata->fieldName] = $propertyMetadata->getValue($object); + } + } + } finally { + unset($this->callStack->objects[$objectId]); + } + + return $data; + } +} diff --git a/tests/Benchmark/GeneratedHydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php index 571758d8..deb1fe63 100644 --- a/tests/Benchmark/GeneratedHydratorBench.php +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -5,7 +5,8 @@ namespace Patchlevel\Hydrator\Tests\Benchmark; use Patchlevel\Hydrator\CoreExtension; -use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\TransformerCompiler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -20,17 +21,15 @@ final class GeneratedHydratorBench public function __construct() { - $this->hydrator = (new StackHydratorBuilder()) + $cachePath = __DIR__ . '/../../var/cache'; + + $builder = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension( - __DIR__ . '/../../var/cache', - [ - ProfileCreated::class, - Skill::class, - ], - debug: true, - )) - ->buildHydrator(); + ->useExtension(new GeneratedTransformerExtension($cachePath)); + + (new TransformerCompiler($builder->metadataFactory(), $cachePath))->compile([ProfileCreated::class]); + + $this->hydrator = $builder->buildHydrator(); } public function setUp(): void diff --git a/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php index 44e9a406..42ad8dd2 100644 --- a/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php +++ b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php @@ -8,7 +8,8 @@ use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; use Patchlevel\Hydrator\Extension\Cryptography\Store\InMemoryCipherKeyStore; -use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\TransformerCompiler; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -27,11 +28,16 @@ public function __construct() { $this->store = new InMemoryCipherKeyStore(); - $this->hydrator = (new StackHydratorBuilder()) + $cachePath = __DIR__ . '/../../var/cache'; + + $builder = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension(__DIR__ . '/../../var/cache', [ProfileCreated::class, Skill::class], debug: true)) - ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))) - ->buildHydrator(); + ->useExtension(new GeneratedTransformerExtension($cachePath)) + ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))); + + (new TransformerCompiler($builder->metadataFactory(), $cachePath))->compile([ProfileCreated::class]); + + $this->hydrator = $builder->buildHydrator(); } public function setUp(): void diff --git a/tests/Unit/Extension/Generated/GeneratedMiddlewareExtensionTest.php b/tests/Unit/Extension/Generated/GeneratedMiddlewareExtensionTest.php deleted file mode 100644 index 48e6d260..00000000 --- a/tests/Unit/Extension/Generated/GeneratedMiddlewareExtensionTest.php +++ /dev/null @@ -1,165 +0,0 @@ -defaultClassName(), (new GeneratedMiddlewareExtension($path, [ProfileCreated::class]))->defaultClassName()); - self::assertNotSame($a->defaultClassName(), $b->defaultClassName()); - } - - public function testGeneratedFileIsCachedAndReused(): void - { - $path = GeneratedStackHydratorTest::cachePath() . '/' . uniqid('cache', true); - $className = 'Cached_' . uniqid(); - $file = sprintf('%s/%s.php', $path, $className); - - $extension = new GeneratedMiddlewareExtension($path, [ProfileCreated::class], className: $className); - $hydrator = (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($extension)->buildHydrator(); - - self::assertFileExists($file); - $mtime = filemtime($file); - touch($file, $mtime - 100); - - // the class is already loaded, the file is neither regenerated nor required again - (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($extension)->buildHydrator(); - clearstatcache(true, $file); - self::assertSame($mtime - 100, filemtime($file)); - - $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); - self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); - } - - public function testMiddlewareStack(): void - { - $hydrator = (new StackHydratorBuilder()) - ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension(GeneratedStackHydratorTest::cachePath(), [ProfileCreated::class])) - ->buildHydrator(); - - self::assertInstanceOf(GeneratedHydrator::class, $hydrator); - self::assertStringStartsWith(GeneratedMiddlewareExtension::NAMESPACE . '\\', $hydrator->middleware()::class); - self::assertSame($hydrator, $hydrator->stack()->rootHydrator()); - - $middlewares = $hydrator->stack()->middlewares(); - - self::assertCount(2, $middlewares); - self::assertInstanceOf(GeneratedMiddlewareSlot::class, $middlewares[0]); - self::assertTrue($middlewares[0]->holds($hydrator->middleware())); - self::assertInstanceOf(TransformMiddleware::class, $middlewares[1]); - } - - public function testBuildIsNotSupported(): void - { - $this->expectException(DecoratorsNotApplied::class); - - (new StackHydratorBuilder()) - ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension(GeneratedStackHydratorTest::cachePath(), [ProfileCreated::class])) - ->build(); - } - - public function testSlotWithoutGeneratedMiddleware(): void - { - $this->expectException(GeneratedMiddlewareNotLoaded::class); - - (new GeneratedMiddlewareSlot())->skip((new StackHydrator())->metadata(ProfileCreated::class)); - } - - public function testTracingWrapsTheGeneratedHydrator(): void - { - $tracer = new RecordingTracer(); - - $hydrator = (new StackHydratorBuilder()) - ->useExtension(new CoreExtension()) - ->useExtension(new GeneratedMiddlewareExtension(GeneratedStackHydratorTest::cachePath(), [NestedLifecycleDto::class])) - ->useExtension(new TracingExtension($tracer)) - ->buildHydrator(); - - $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; - self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); - - // nested objects are not inlined, so they go through the tracing as well - self::assertSame( - [ - 'hydrate ' . NestedLifecycleDto::class, - 'hydrate ' . LifecycleFixture::class, - 'hydrate ' . LifecycleFixture::class, - 'extract ' . NestedLifecycleDto::class, - 'extract ' . LifecycleFixture::class, - 'extract ' . LifecycleFixture::class, - ], - $tracer->traces, - ); - } - - public function testCodeIsGeneratedWithTheCompleteMetadataFactory(): void - { - // the extension is registered before the CoreExtension, so its guesser for the date is not known yet - // when the extension is configured. The code has to be generated when the hydrator is built. - $hydrator = (new StackHydratorBuilder()) - ->useExtension(new GeneratedMiddlewareExtension(GeneratedStackHydratorTest::cachePath(), [InferredDateDto::class], debug: true)) - ->useExtension(new CoreExtension()) - ->buildHydrator(); - - $data = ['createdAt' => '2024-05-04T10:15:30+00:00']; - $object = $hydrator->hydrate(InferredDateDto::class, $data); - - self::assertSame('2024-05-04T10:15:30+00:00', $object->createdAt->format(DateTimeInterface::ATOM)); - self::assertSame($data, $hydrator->extract($object)); - } - - public function testNotWritable(): void - { - $this->expectException(GeneratedMiddlewareNotWritable::class); - - $file = sys_get_temp_dir() . '/' . uniqid('not-a-directory'); - touch($file); - - (new StackHydratorBuilder()) - ->useExtension(new GeneratedMiddlewareExtension($file, [ProfileCreated::class], className: 'NotWritable_' . uniqid())) - ->buildHydrator(); - } -} diff --git a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php index 4be40367..2ff272a7 100644 --- a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php +++ b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php @@ -17,11 +17,12 @@ use Patchlevel\Hydrator\Extension\Generated\ClassPlan; use Patchlevel\Hydrator\Extension\Generated\ClassPlanner; use Patchlevel\Hydrator\Extension\Generated\CodeEmitter; -use Patchlevel\Hydrator\Extension\Generated\GeneratedHydrator; -use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; -use Patchlevel\Hydrator\Extension\Generated\MiddlewareGenerator; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerFactory; use Patchlevel\Hydrator\Extension\Generated\PropertyPlan; use Patchlevel\Hydrator\Extension\Generated\Templates; +use Patchlevel\Hydrator\Extension\Generated\TransformerFiles; +use Patchlevel\Hydrator\Extension\Generated\TransformerGenerator; use Patchlevel\Hydrator\Extension\Generated\ValueKind; use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension; use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster; @@ -29,9 +30,9 @@ use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\ClassMetadata; +use Patchlevel\Hydrator\Metadata\MetadataEnricher; use Patchlevel\Hydrator\Middleware\Middleware; use Patchlevel\Hydrator\Middleware\Skip; -use Patchlevel\Hydrator\Middleware\SkippableMiddleware; use Patchlevel\Hydrator\Middleware\Stack; use Patchlevel\Hydrator\NormalizationFailure; use Patchlevel\Hydrator\StackHydrator; @@ -65,6 +66,8 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\StatusWithNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\ValueObject; use Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer; +use Patchlevel\Hydrator\Transformer\ClassTransformer; +use Patchlevel\Hydrator\Transformer\ReflectionTransformer; use Patchlevel\Hydrator\TypeMismatch; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -72,19 +75,22 @@ use ReflectionClass; use function array_filter; -use function array_values; use function get_object_vars; +use function glob; use function preg_match; use function sys_get_temp_dir; +use function uniqid; +use function unlink; use const ARRAY_FILTER_USE_KEY; -use const PHP_VERSION_ID; /** - * Mirrors the StackHydratorTest: the generated middleware must behave exactly like the TransformMiddleware. + * Mirrors the StackHydratorTest: the generated transformer must behave exactly like the ReflectionTransformer. */ -#[CoversClass(GeneratedMiddlewareExtension::class)] -#[CoversClass(MiddlewareGenerator::class)] +#[CoversClass(GeneratedTransformerExtension::class)] +#[CoversClass(GeneratedTransformerFactory::class)] +#[CoversClass(TransformerFiles::class)] +#[CoversClass(TransformerGenerator::class)] #[CoversClass(ClassPlanner::class)] #[CoversClass(ClassPlan::class)] #[CoversClass(PropertyPlan::class)] @@ -93,6 +99,7 @@ #[CoversClass(Templates::class)] final class GeneratedStackHydratorTest extends TestCase { + /** Classes covered by the snapshot of the generated code. */ public const CLASSES = [ ProfileCreated::class, ParentDto::class, @@ -119,45 +126,40 @@ final class GeneratedStackHydratorTest extends TestCase public function setUp(): void { + self::clearCache(); + $this->hydrator = $this->builder()->buildHydrator(); } - /** - * Closures in attributes (used by the inline normalizer fixture) are only supported since PHP 8.5. - * - * @return list - */ - private static function supportedClasses(): array + private function builder(): StackHydratorBuilder { - if (PHP_VERSION_ID >= 80500) { - return self::CLASSES; - } + return (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($this->extension()); + } - return array_values(array_filter( - self::CLASSES, - static fn (string $class): bool => $class !== ProfileCreatedWithInlineNormalizer::class, - )); + private function extension(): GeneratedTransformerExtension + { + return new GeneratedTransformerExtension(self::cachePath(), autoGenerate: true); } - /** @param list|null $classes */ - private function builder(array|null $classes = null): StackHydratorBuilder + public static function cachePath(): string { - return (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($this->extension($classes)); + return sys_get_temp_dir() . '/patchlevel-hydrator-tests'; } - /** @param list|null $classes */ - private function extension(array|null $classes = null): GeneratedMiddlewareExtension + /** Removes the generated files, the generator may have changed since they were written. */ + public static function clearCache(): void { - return new GeneratedMiddlewareExtension( - self::cachePath(), - $classes ?? self::supportedClasses(), - debug: true, - ); + foreach (glob(self::cachePath() . '/*.php') ?: [] as $file) { + unlink($file); + } } - public static function cachePath(): string + /** @param class-string $class */ + private static function transformer(Hydrator $hydrator, string $class): ClassTransformer { - return sys_get_temp_dir() . '/patchlevel-hydrator-tests'; + self::assertInstanceOf(StackHydrator::class, $hydrator); + + return $hydrator->transformer($hydrator->metadata($class)); } public function testExtract(): void @@ -688,7 +690,7 @@ public function testHydratePrivatePropertiesOfParent(): void public function testHydrateAsymmetricVisibility(): void { $data = ['name' => 'foo', 'age' => 12, 'note' => 'note']; - $hydrator = $this->builder([AsymmetricVisibilityDto::class])->buildHydrator(); + $hydrator = $this->builder()->buildHydrator(); $dto = $hydrator->hydrate(AsymmetricVisibilityDto::class, $data); @@ -706,17 +708,41 @@ public function testHydrateCoercesScalarsLikeReflection(): void self::assertSame(12, $dto->age()); } - public function testHydrateUnknownClassFallsBackToTransformMiddleware(): void + public function testHydrateWithoutGeneratedCodeFallsBackToReflection(): void { - $hydrator = $this->builder([ProfileCreated::class])->buildHydrator(); + // a unique field name gives the class a fingerprint no transformer was generated or loaded for yet + $field = uniqid('email_'); + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension(self::cachePath())) + ->addMetadataEnricher(new class ($field) implements MetadataEnricher { + public function __construct(private readonly string $field) + { + } + + public function enrich(ClassMetadata $classMetadata): void + { + $property = $classMetadata->properties['email'] ?? null; + + if ($property === null) { + return; + } + + $property->fieldName = $this->field; + } + }) + ->buildHydrator(); $event = $hydrator->hydrate( ParentDto::class, - ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ['profileId' => '1', $field => 'info@patchlevel.de'], ); self::assertEquals(new ParentDto(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); - self::assertEquals(['profileId' => '1', 'email' => 'info@patchlevel.de'], $hydrator->extract($event)); + self::assertEquals(['profileId' => '1', $field => 'info@patchlevel.de'], $hydrator->extract($event)); + self::assertInstanceOf(ReflectionTransformer::class, self::transformer($hydrator, ParentDto::class)); + self::assertSame([], glob(self::cachePath() . '/*.php')); } #[RequiresPhp('>=8.4')] @@ -843,17 +869,20 @@ public function testEncrypt(): void } /** - * Reads the inlining decisions of the generated middleware. + * Reads the inlining decisions of the generated transformer. + * + * @param class-string $class * * @return array flag name => inlined */ - private static function inlined(Hydrator $hydrator): array + private static function inlined(Hydrator $hydrator, string $class = NestedLifecycleDto::class): array { - self::assertInstanceOf(GeneratedHydrator::class, $hydrator); + $transformer = self::transformer($hydrator, $class); + self::assertNotInstanceOf(ReflectionTransformer::class, $transformer); /** @var array $flags */ $flags = array_filter( - get_object_vars($hydrator->middleware()), + get_object_vars($transformer), static fn (string $name): bool => preg_match('/^i[he]\d+$/', $name) === 1, ARRAY_FILTER_USE_KEY, ); @@ -863,7 +892,7 @@ private static function inlined(Hydrator $hydrator): array public function testNestedObjectsAreInlinedWithoutOtherMiddlewares(): void { - $hydrator = $this->builder([NestedLifecycleDto::class])->buildHydrator(); + $hydrator = $this->builder()->buildHydrator(); $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); @@ -876,7 +905,7 @@ public function testNestedObjectsAreInlinedWhenOtherMiddlewaresSkipThem(): void $hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension($this->extension([NestedLifecycleDto::class])) + ->useExtension($this->extension()) ->addMiddleware($counter) ->buildHydrator(); @@ -905,7 +934,7 @@ public function upcast(ClassMetadata $metadata, array $data, array $context): ar $hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension($this->extension([NestedLifecycleDto::class])) + ->useExtension($this->extension()) ->useExtension(new UpcastExtension([$upcaster])) ->buildHydrator(); @@ -916,7 +945,7 @@ public function upcast(ClassMetadata $metadata, array $data, array $context): ar // callback upcasters for other classes do not prevent inlining $hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension($this->extension([NestedLifecycleDto::class])) + ->useExtension($this->extension()) ->useExtension(new UpcastExtension([CallbackUpcaster::forClass(ProfileCreated::class, static fn (array $data): array => $data)])) ->buildHydrator(); @@ -924,45 +953,31 @@ public function upcast(ClassMetadata $metadata, array $data, array $context): ar self::assertSame(['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], self::inlined($hydrator)); } - public function testCompiledClosures(): void + public function testGeneratedTransformerIsUsed(): void { - $hydrator = $this->builder([ProfileCreated::class])->buildHydrator(); + $hydrator = $this->builder()->buildHydrator(); - self::assertInstanceOf(GeneratedHydrator::class, $hydrator); - $middleware = $hydrator->middleware(); - $stack = $hydrator->stack(); + $transformer = self::transformer($hydrator, ProfileCreated::class); + + self::assertStringStartsWith(TransformerFiles::NAMESPACE . '\\ProfileCreatedTransformer_', $transformer::class); + } - self::assertNotNull($middleware->compiledHydrator($stack->metadata(ProfileCreated::class))); - self::assertNotNull($middleware->compiledExtractor($stack->metadata(ProfileCreated::class))); - self::assertNull($middleware->compiledHydrator($stack->metadata(ParentDto::class))); - self::assertNull($middleware->compiledExtractor($stack->metadata(ParentDto::class))); + public function testStackRunsWithOtherMiddlewares(): void + { + $counter = new CountingMiddleware(); - // with a middleware which does not skip the class, the stack has to run $hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) - ->useExtension($this->extension([ProfileCreated::class])) - ->addMiddleware(new CountingMiddleware()) + ->useExtension($this->extension()) + ->addMiddleware($counter) ->buildHydrator(); - self::assertInstanceOf(GeneratedHydrator::class, $hydrator); - $middleware = $hydrator->middleware(); - $stack = $hydrator->stack(); - - self::assertNull($middleware->compiledHydrator($stack->metadata(ProfileCreated::class))); - self::assertNull($middleware->compiledExtractor($stack->metadata(ProfileCreated::class))); - } - - public function testUnknownClassesAreSkipped(): void - { - $hydrator = $this->builder([ProfileCreated::class])->buildHydrator(); - - self::assertInstanceOf(GeneratedHydrator::class, $hydrator); - $middleware = $hydrator->middleware(); - $stack = $hydrator->stack(); + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + $hydrator->extract($event); - self::assertInstanceOf(SkippableMiddleware::class, $middleware); - self::assertSame(Skip::None, $middleware->skip($stack->metadata(ProfileCreated::class))); - self::assertSame(Skip::Both, $middleware->skip($stack->metadata(ParentDto::class))); + self::assertSame([ProfileCreated::class => 1], $counter->hydrated); + self::assertSame([ProfileCreated::class => 1], $counter->extracted); + self::assertNotInstanceOf(ReflectionTransformer::class, self::transformer($hydrator, ProfileCreated::class)); } public function testNestedObjectsAreNotInlinedWithNonSkippableMiddleware(): void diff --git a/tests/Unit/Extension/Generated/GeneratedTransformerExtensionTest.php b/tests/Unit/Extension/Generated/GeneratedTransformerExtensionTest.php new file mode 100644 index 00000000..579c9002 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedTransformerExtensionTest.php @@ -0,0 +1,220 @@ +useExtension(new GeneratedTransformerExtension('/tmp')); + + self::assertCount(1, $builder->transformerFactories()); + self::assertInstanceOf(GeneratedTransformerFactory::class, $builder->transformerFactories()[0]); + self::assertSame([], $builder->decorators()); + } + + public function testGeneratedFileIsWrittenAndReused(): void + { + $path = self::uniquePath(); + $field = uniqid('email_'); + + $hydrator = self::hydrator(new GeneratedTransformerExtension($path, autoGenerate: true), $field); + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', $field => 'info@patchlevel.de']); + + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + + $files = glob($path . '/ProfileCreatedTransformer_*.php'); + self::assertIsArray($files); + self::assertCount(1, $files); + + $file = $files[0]; + $mtime = filemtime($file); + self::assertIsInt($mtime); + touch($file, $mtime - 100); + + // the class is already loaded, the file is neither generated nor required again + $hydrator = self::hydrator(new GeneratedTransformerExtension($path, autoGenerate: true), $field); + $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', $field => 'info@patchlevel.de']); + + clearstatcache(true, $file); + self::assertSame($mtime - 100, filemtime($file)); + self::assertNotInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(ProfileCreated::class))); + } + + public function testNothingIsGeneratedWithoutAutoGenerate(): void + { + $path = self::uniquePath(); + + $field = uniqid('email_'); + + $hydrator = self::hydrator(new GeneratedTransformerExtension($path), $field); + $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', $field => 'info@patchlevel.de']); + + self::assertInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(ProfileCreated::class))); + self::assertDirectoryDoesNotExist($path); + } + + public function testAnonymousClassesAreTransformedWithReflection(): void + { + $object = new class { + public string $name = 'foo'; + }; + + $hydrator = self::hydrator(new GeneratedTransformerExtension(self::uniquePath(), autoGenerate: true)); + + self::assertSame(['name' => 'foo'], $hydrator->extract($object)); + self::assertInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata($object::class))); + } + + public function testNoTransformerForClassesWhichAreNeverTransformed(): void + { + $hydrator = self::hydrator(new GeneratedTransformerExtension(self::uniquePath(), autoGenerate: true)); + $factory = new GeneratedTransformerFactory(new TransformerFiles(self::uniquePath()), true); + + // a class normalizer handles the class, the hydrator never asks for its transformer + self::assertNull($factory->create($hydrator->metadata(ProfileId::class), $hydrator)); + } + + public function testBuildIsSupported(): void + { + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension(GeneratedStackHydratorTest::cachePath(), autoGenerate: true)) + ->build(); + + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertNotInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(ProfileCreated::class))); + } + + public function testTracingSeesNestedObjects(): void + { + $tracer = new RecordingTracer(); + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension(GeneratedStackHydratorTest::cachePath(), autoGenerate: true)) + ->useExtension(new TracingExtension($tracer)) + ->buildHydrator(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + + // nested objects are not inlined, so they go through the tracing as well + self::assertSame( + [ + 'hydrate ' . NestedLifecycleDto::class, + 'hydrate ' . LifecycleFixture::class, + 'hydrate ' . LifecycleFixture::class, + 'extract ' . NestedLifecycleDto::class, + 'extract ' . LifecycleFixture::class, + 'extract ' . LifecycleFixture::class, + ], + $tracer->traces, + ); + } + + public function testCodeIsGeneratedWithTheCompleteMetadataFactory(): void + { + // the extension is registered before the CoreExtension, so its guesser for the date is not known yet + // when the extension is configured. The code is generated with the metadata of the hydrator. + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension(GeneratedStackHydratorTest::cachePath(), autoGenerate: true)) + ->useExtension(new CoreExtension()) + ->buildHydrator(); + + $data = ['createdAt' => '2024-05-04T10:15:30+00:00']; + $object = $hydrator->hydrate(InferredDateDto::class, $data); + + self::assertSame('2024-05-04T10:15:30+00:00', $object->createdAt->format(DateTimeInterface::ATOM)); + self::assertSame($data, $hydrator->extract($object)); + } + + public function testNotWritable(): void + { + $file = sys_get_temp_dir() . '/' . uniqid('not-a-directory'); + touch($file); + + $field = uniqid('email_'); + $hydrator = self::hydrator(new GeneratedTransformerExtension($file, autoGenerate: true), $field); + + $this->expectException(GeneratedTransformerNotWritable::class); + + $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', $field => 'info@patchlevel.de']); + } + + /** + * Classes are loaded once per process, renaming the email field to a unique name gives the class a fingerprint + * no transformer was loaded for yet. + */ + private static function hydrator(GeneratedTransformerExtension $extension, string|null $emailField = null): StackHydrator + { + $builder = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($extension); + + if ($emailField !== null) { + $builder->addMetadataEnricher(new class ($emailField) implements MetadataEnricher { + public function __construct(private readonly string $field) + { + } + + public function enrich(ClassMetadata $classMetadata): void + { + $property = $classMetadata->properties['email'] ?? null; + + if ($property === null) { + return; + } + + $property->fieldName = $this->field; + } + }); + } + + $hydrator = $builder->buildHydrator(); + self::assertInstanceOf(StackHydrator::class, $hydrator); + + return $hydrator; + } + + private static function uniquePath(): string + { + return GeneratedStackHydratorTest::cachePath() . '/' . uniqid('cache', true); + } +} diff --git a/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php b/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php deleted file mode 100644 index 664d5cbb..00000000 --- a/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php +++ /dev/null @@ -1,226 +0,0 @@ -dump([ProfileCreated::class, Skill::class], 'Foo\\Bar'); - $second = $generator->dump([ProfileCreated::class, Skill::class], 'Foo\\Bar'); - - self::assertSame($first, $second); - self::assertStringContainsString('namespace Foo;', $first); - self::assertStringContainsString('final class Bar implements SkippableMiddleware, HydratorAwareMiddleware', $first); - self::assertStringNotContainsString('strict_types=1', $first); - } - - /** - * The snapshot makes every change of the generated code visible in the diff of a pull request. - * Run "make snapshot" after an intended change of the generator. - */ - #[RequiresPhp('>=8.5')] - public function testGeneratedCodeMatchesSnapshot(): void - { - $file = __DIR__ . '/Snapshot/SnapshotMiddleware.php'; - $code = (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump( - GeneratedStackHydratorTest::CLASSES, - 'Patchlevel\\Hydrator\\Tests\\Snapshot\\SnapshotMiddleware', - ); - - if (getenv('UPDATE_SNAPSHOTS') === '1') { - file_put_contents($file, $code); - } - - self::assertStringEqualsFile($file, $code, 'The generated code changed, run "make snapshot" if the change is intended.'); - } - - public function testClassesWithClassNormalizerAreSkipped(): void - { - $generator = new MiddlewareGenerator(new AttributeMetadataFactory()); - - $code = $generator->dump([ProfileId::class], 'Foo\\Bar'); - - self::assertStringNotContainsString(sprintf('\\%s::class', ProfileId::class), $code); - } - - public function testAbstractClassIsNotGeneratable(): void - { - $this->expectException(ClassNotGeneratable::class); - - (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump([ChildDto::class], 'Foo\\Bar'); - } - - public function testUnknownClassIsNotGeneratable(): void - { - $this->expectException(ClassNotGeneratable::class); - - /** @phpstan-ignore argument.type */ - (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump(['Unknown'], 'Foo\\Bar'); - } - - public function testMiddlewareRequiresHydrator(): void - { - $middleware = $this->load([ProfileCreated::class]); - $metadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class); - - $this->expectException(HydratorNotSet::class); - - $middleware->hydrate($metadata, ['profileId' => '1', 'email' => 'info@patchlevel.de'], [], new Stack([$middleware])); - } - - public function testFailedInitializationIsRetried(): void - { - $middleware = $this->load([ProfileCreated::class]); - $metadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class); - $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; - - try { - $middleware->hydrate($metadata, $data, [], new Stack([$middleware])); - self::fail('the first call must fail, the hydrator is not set yet'); - } catch (HydratorNotSet) { - } - - // the failed initialization must not leave the class half initialized - $hydrator = (new StackHydratorBuilder())->addMiddleware($middleware)->build(); - $object = $hydrator->hydrate(ProfileCreated::class, $data); - - self::assertSame('info@patchlevel.de', $object->email->toString()); - } - - public function testDefaultsWithControlCharactersAreInlinedUnchanged(): void - { - $middleware = $this->load([MultilineDefaultDto::class]); - $hydrator = (new StackHydratorBuilder())->addMiddleware($middleware)->build(); - - $object = $hydrator->hydrate(MultilineDefaultDto::class, []); - - self::assertSame("foo bar\nbaz", $object->name); - self::assertSame("first\nsecond", $object->text); - self::assertSame(["a\tb" => "c\nd \$e \"f\""], $object->lines); - } - - /** @param class-string $changed a class with the same properties as FingerprintDto but one changed detail */ - #[DataProvider('changedClasses')] - public function testChangedClassIsDetected(string $changed): void - { - // generated for FingerprintDto, then pointed at the changed class, like a class changing after generation - $middleware = $this->load( - [FingerprintDto::class], - static fn (string $code): string => str_replace(FingerprintDto::class, $changed, $code), - ); - $hydrator = (new StackHydratorBuilder())->addMiddleware($middleware)->build(); - - $this->expectException(OutdatedGeneratedMiddleware::class); - - $hydrator->hydrate($changed, ['name' => 'foo']); - } - - /** @return iterable */ - public static function changedClasses(): iterable - { - yield 'changed default' => [FingerprintChangedDefaultDto::class]; - yield 'changed visibility' => [FingerprintChangedVisibilityDto::class]; - } - - public function testOutdatedMiddlewareIsDetected(): void - { - $middleware = $this->load([ProfileCreated::class]); - - $hydrator = (new StackHydratorBuilder()) - ->addMiddleware($middleware) - ->addMetadataEnricher(new class implements MetadataEnricher { - public function enrich(ClassMetadata $classMetadata): void - { - foreach ($classMetadata->properties as $property) { - $property->fieldName = 'renamed_' . $property->fieldName; - } - } - }) - ->build(); - - $this->expectException(OutdatedGeneratedMiddleware::class); - - $hydrator->hydrate(ProfileCreated::class, ['renamed_profileId' => '1', 'renamed_email' => 'info@patchlevel.de']); - } - - /** - * @param list $classes - * @param (callable(string): string)|null $transform applied to the generated code before it is loaded - */ - private function load(array $classes, callable|null $transform = null): Middleware - { - $fqcn = 'Patchlevel\\Hydrator\\Tests\\Generated\\Middleware' . self::$counter++; - $code = (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump($classes, $fqcn); - - if ($transform !== null) { - $code = $transform($code); - } - - $file = tempnam(GeneratedStackHydratorTest::cachePath(), 'middleware'); - assert($file !== false); - file_put_contents($file, $code); - - require $file; - unlink($file); - - assert(is_a($fqcn, Middleware::class, true)); - - return new $fqcn(); - } -} diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php new file mode 100644 index 00000000..5b2a504e --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php @@ -0,0 +1,514 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih2 = false; + public bool $ie2 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n1; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + public Normalizer $n2; + public bool $ih0 = false; + public bool $ie0 = false; + public bool $ready2 = false; + public bool $initializing2 = false; + public bool|null $recursive2 = null; + public Closure $h2; + public Closure $e2; + public ReflectionClass $r2; + public bool $tracked2 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; + $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'to'); + $this->ih2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih2 || $this->ie2) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class); + $this->r1 = $metadata->reflection; + $this->n1 = $this->normalizer($metadata, 'to'); + $this->ih1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + if ($this->ih1 || $this->ie1) { $this->init2(); } + $this->tracked1 = $this->recursive1(); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + private function init2(): void + { + if ($this->ready2 || $this->initializing2) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing2 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class); + $this->r2 = $metadata->reflection; + $this->n2 = $this->normalizer($metadata, 'to'); + $this->ih0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h2 = $this->hydrate2(...); + $this->e2 = $this->extract2(...); + if ($this->ih0 || $this->ie0) { $this->init0(); } + $this->tracked2 = $this->recursive2(); + + $this->ready2 = true; + } finally { + $this->initializing2 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih2 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie2 ? $this->recursive1() : $this->n0 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih1 && \is_array($value)) { + $value = $this->hydrate2($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n1::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $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\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = ($this->ie1 ? $this->recursive2() : $this->n1 instanceof HydratorAwareNormalizer); + } + + public function hydrate2(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r2->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate0($value, $context); + } else { + $value = $this->n2->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n2::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract2(object $object, array $context): array + { + if ($this->tracked2) { + $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\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive2(): bool + { + if ($this->recursive2 !== null) { + return $this->recursive2; + } + + $this->recursive2 = true; + + return $this->recursive2 = ($this->ie0 ? $this->recursive0() : $this->n2 instanceof HydratorAwareNormalizer); + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php new file mode 100644 index 00000000..608c411d --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php @@ -0,0 +1,514 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih2 = false; + public bool $ie2 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n1; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + public Normalizer $n2; + public bool $ih0 = false; + public bool $ie0 = false; + public bool $ready2 = false; + public bool $initializing2 = false; + public bool|null $recursive2 = null; + public Closure $h2; + public Closure $e2; + public ReflectionClass $r2; + public bool $tracked2 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; + $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'to'); + $this->ih2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih2 || $this->ie2) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class); + $this->r1 = $metadata->reflection; + $this->n1 = $this->normalizer($metadata, 'to'); + $this->ih1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + if ($this->ih1 || $this->ie1) { $this->init2(); } + $this->tracked1 = $this->recursive1(); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + private function init2(): void + { + if ($this->ready2 || $this->initializing2) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing2 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class); + $this->r2 = $metadata->reflection; + $this->n2 = $this->normalizer($metadata, 'to'); + $this->ih0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h2 = $this->hydrate2(...); + $this->e2 = $this->extract2(...); + if ($this->ih0 || $this->ie0) { $this->init0(); } + $this->tracked2 = $this->recursive2(); + + $this->ready2 = true; + } finally { + $this->initializing2 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih2 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie2 ? $this->recursive1() : $this->n0 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih1 && \is_array($value)) { + $value = $this->hydrate2($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n1::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $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\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = ($this->ie1 ? $this->recursive2() : $this->n1 instanceof HydratorAwareNormalizer); + } + + public function hydrate2(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r2->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate0($value, $context); + } else { + $value = $this->n2->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n2::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract2(object $object, array $context): array + { + if ($this->tracked2) { + $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\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive2(): bool + { + if ($this->recursive2 !== null) { + return $this->recursive2; + } + + $this->recursive2 = true; + + return $this->recursive2 = ($this->ie0 ? $this->recursive0() : $this->n2 instanceof HydratorAwareNormalizer); + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php new file mode 100644 index 00000000..372653cc --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php @@ -0,0 +1,514 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih2 = false; + public bool $ie2 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n1; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + public Normalizer $n2; + public bool $ih0 = false; + public bool $ie0 = false; + public bool $ready2 = false; + public bool $initializing2 = false; + public bool|null $recursive2 = null; + public Closure $h2; + public Closure $e2; + public ReflectionClass $r2; + public bool $tracked2 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; + $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'to'); + $this->ih2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie2 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih2 || $this->ie2) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class); + $this->r1 = $metadata->reflection; + $this->n1 = $this->normalizer($metadata, 'to'); + $this->ih1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie1 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + if ($this->ih1 || $this->ie1) { $this->init2(); } + $this->tracked1 = $this->recursive1(); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + private function init2(): void + { + if ($this->ready2 || $this->initializing2) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing2 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class); + $this->r2 = $metadata->reflection; + $this->n2 = $this->normalizer($metadata, 'to'); + $this->ih0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n2, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396', Skip::Extract); + $this->h2 = $this->hydrate2(...); + $this->e2 = $this->extract2(...); + if ($this->ih0 || $this->ie0) { $this->init0(); } + $this->tracked2 = $this->recursive2(); + + $this->ready2 = true; + } finally { + $this->initializing2 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih2 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie2 ? $this->recursive1() : $this->n0 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih1 && \is_array($value)) { + $value = $this->hydrate2($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n1::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $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\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract2($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n1::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = ($this->ie1 ? $this->recursive2() : $this->n1 instanceof HydratorAwareNormalizer); + } + + public function hydrate2(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r2->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate0($value, $context); + } else { + $value = $this->n2->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n2::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract2(object $object, array $context): array + { + if ($this->tracked2) { + $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\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract0($value, $context) : $this->n2->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n2::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive2(): bool + { + if ($this->recursive2 !== null) { + return $this->recursive2; + } + + $this->recursive2 = true; + + return $this->recursive2 = ($this->ie0 ? $this->recursive0() : $this->n2 instanceof HydratorAwareNormalizer); + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php new file mode 100644 index 00000000..48aa17e8 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php @@ -0,0 +1,255 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'value'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('value', $data)) { + try { + $value = $this->n0->denormalize($data['value'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + try { + $object->value = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\ContextAwareDto::class; + + try { + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php new file mode 100644 index 00000000..e1c879f5 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php @@ -0,0 +1,346 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih0 = false; + public bool $ie0 = false; + public ReflectionParameter $d0; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class); + $this->r0 = $metadata->reflection; + $this->d0 = $this->promotedDefault($metadata, 'email'); + $this->n0 = $this->normalizer($metadata, 'email'); + $this->ih0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, '136535d19b213863', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, '136535d19b213863', Skip::Extract); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih0 || $this->ie0) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + $this->r1 = $metadata->reflection; + $this->h1 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('value', $data)) { + $value = $data['value']; + + try { + $object->value = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, 'value', $e); + } + } + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + $this->e1 = Closure::bind(function (object $object, array $context): array { + + + return [ + 'value' => $object->value, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('admin', $data)) { + $value = $data['admin']; + + try { + $object->admin = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'admin', $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\DefaultDto::class, 'name', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $data['email']; + + if ($this->ih0 && \is_array($value)) { + $value = ($this->h1)($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $e); + } + } else { + $object->email = $this->d0->getDefaultValue(); + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e1)($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->email; + $v_email = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e1)($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie0 ? false : $this->n0 instanceof HydratorAwareNormalizer); + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php new file mode 100644 index 00000000..b9f29bfc --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php @@ -0,0 +1,372 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public Normalizer $n2; + public Normalizer $n3; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'status'); + $this->n1 = $this->normalizer($metadata, 'dateTimeImmutable'); + $this->n2 = $this->normalizer($metadata, 'dateTime'); + $this->n3 = $this->normalizer($metadata, 'dateTimeZone'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $e); + } + } + + if (\array_key_exists('dateTimeImmutable', $data)) { + try { + $value = $this->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $object->dateTimeImmutable = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $e); + } + } + + if (\array_key_exists('dateTime', $data)) { + try { + $value = $this->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $object->dateTime = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $e); + } + } + + if (\array_key_exists('dateTimeZone', $data)) { + try { + $value = $this->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $object->dateTimeZone = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $e); + } + } + + if (\array_key_exists('array', $data)) { + $value = $data['array']; + + try { + $object->array = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'array', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\InferNormalizerDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer + || $this->n2 instanceof HydratorAwareNormalizer + || $this->n3 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php new file mode 100644 index 00000000..5dd8cb0a --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php @@ -0,0 +1,456 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public Normalizer $n2; + public Normalizer $n3; + public Normalizer $n4; + public Normalizer $n5; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'defaultArray'); + $this->n1 = $this->normalizer($metadata, 'listArray'); + $this->n2 = $this->normalizer($metadata, 'iterableArray'); + $this->n3 = $this->normalizer($metadata, 'hashMap'); + $this->n4 = $this->normalizer($metadata, 'nested'); + $this->n5 = $this->normalizer($metadata, 'shapeArray'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n0->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $object->defaultArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $e); + } + } else { + $object->defaultArray = []; + } + + if (\array_key_exists('listArray', $data)) { + try { + $value = $this->n1->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $object->listArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $e); + } + } else { + $object->listArray = []; + } + + if (\array_key_exists('iterableArray', $data)) { + try { + $value = $this->n2->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $object->iterableArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $e); + } + } else { + $object->iterableArray = []; + } + + if (\array_key_exists('hashMap', $data)) { + try { + $value = $this->n3->denormalize($data['hashMap'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $object->hashMap = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $e); + } + } else { + $object->hashMap = []; + } + + if (\array_key_exists('nested', $data)) { + try { + $value = $this->n4->denormalize($data['nested'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $object->nested = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $e); + } + } else { + $object->nested = []; + } + + if (\array_key_exists('jsonArray', $data)) { + $value = $data['jsonArray']; + + try { + $object->jsonArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'jsonArray', $e); + } + } else { + $object->jsonArray = NULL; + } + + if (\array_key_exists('shapeArray', $data)) { + try { + $value = $this->n5->denormalize($data['shapeArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::class, $e); + } + + try { + $object->shapeArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $e); + } + } else { + $object->shapeArray = NULL; + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\InferNormalizerWithIterablesDto::class; + + try { + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer + || $this->n2 instanceof HydratorAwareNormalizer + || $this->n3 instanceof HydratorAwareNormalizer + || $this->n4 instanceof HydratorAwareNormalizer + || $this->n5 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php new file mode 100644 index 00000000..28d60df0 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php @@ -0,0 +1,401 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public Normalizer $n2; + public Normalizer $n3; + public Normalizer $n4; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'status'); + $this->n1 = $this->normalizer($metadata, 'dateTimeImmutable'); + $this->n2 = $this->normalizer($metadata, 'dateTime'); + $this->n3 = $this->normalizer($metadata, 'dateTimeZone'); + $this->n4 = $this->normalizer($metadata, 'profileId'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $e); + } + } + + if (\array_key_exists('dateTimeImmutable', $data)) { + try { + $value = $this->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $object->dateTimeImmutable = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $e); + } + } + + if (\array_key_exists('dateTime', $data)) { + try { + $value = $this->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $object->dateTime = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $e); + } + } else { + $object->dateTime = NULL; + } + + if (\array_key_exists('dateTimeZone', $data)) { + try { + $value = $this->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $object->dateTimeZone = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $e); + } + } else { + $object->dateTimeZone = NULL; + } + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n4->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $e); + } + } else { + $object->profileId = NULL; + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\InferNormalizerWithNullableDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer + || $this->n2 instanceof HydratorAwareNormalizer + || $this->n3 instanceof HydratorAwareNormalizer + || $this->n4 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php new file mode 100644 index 00000000..53eafea8 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php @@ -0,0 +1,290 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->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\LazyProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\LazyProfileCreated::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\LazyProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } 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\LazyProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php new file mode 100644 index 00000000..5f28137c --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php @@ -0,0 +1,404 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih0 = false; + public bool $ie0 = false; + public Normalizer $n1; + public ObjectNormalizer $n2; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'child'); + $this->ih0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'f6938c7b12413f3a', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'f6938c7b12413f3a', Skip::Extract); + $this->n1 = $this->normalizer($metadata, 'items'); + $this->ih1 = $this->inlineArray($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'f6938c7b12413f3a', Skip::Hydrate); + $this->ie1 = $this->inlineArray($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'f6938c7b12413f3a', Skip::Extract); + if ($this->ih1 || $this->ie1) { $this->n2 = $this->n1->innerNormalizer(); } + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih0 || $this->ie0) { $this->init1(); } + if ($this->ih1 || $this->ie1) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class); + $this->r1 = $metadata->reflection; + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $object->child = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $e); + } + } + + if (\array_key_exists('items', $data)) { + try { + $value = $data['items']; + + if ($this->ih1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hydrate1($item, $context) : $this->n2->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + try { + $object->items = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $e); + } + } else { + $object->items = []; + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract1($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->child; + $v_child = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract1($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie0 ? false : $this->n0 instanceof HydratorAwareNormalizer) + || ($this->ie1 ? false : $this->n1 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->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\LifecycleFixture::class, 'name', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + + + return [ + 'name' => $object->name, + ]; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php new file mode 100644 index 00000000..e8dab8dc --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php @@ -0,0 +1,709 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ih0 = false; + public bool $ie0 = false; + public Normalizer $n4; + public Normalizer $n5; + public Normalizer $n6; + public Normalizer $n7; + public ObjectNormalizer $n8; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n2; + public Normalizer $n3; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + public bool $ready2 = false; + public bool $initializing2 = false; + public bool|null $recursive2 = null; + public Closure $h2; + public Closure $e2; + public ReflectionClass $r2; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; + $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'status'); + $this->n1 = $this->normalizer($metadata, 'profileCreated'); + $this->ih0 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, '626de5f0f972600f', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, '626de5f0f972600f', Skip::Extract); + $this->n4 = $this->normalizer($metadata, 'defaultArray'); + $this->n5 = $this->normalizer($metadata, 'listArray'); + $this->n6 = $this->normalizer($metadata, 'iterableArray'); + $this->n7 = $this->normalizer($metadata, 'skillsHashMap'); + $this->ih1 = $this->inlineArray($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, 'f6938c7b12413f3a', Skip::Hydrate); + $this->ie1 = $this->inlineArray($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, 'f6938c7b12413f3a', Skip::Extract); + if ($this->ih1 || $this->ie1) { $this->n8 = $this->n7->innerNormalizer(); } + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih0 || $this->ie0) { $this->init1(); } + if ($this->ih1 || $this->ie1) { $this->init2(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class); + $this->r1 = $metadata->reflection; + $this->n2 = $this->normalizer($metadata, 'profileId'); + $this->n3 = $this->normalizer($metadata, 'email'); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + $this->tracked1 = $this->recursive1(); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + private function init2(): void + { + if ($this->ready2 || $this->initializing2) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing2 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class); + $this->r2 = $metadata->reflection; + $this->h2 = $this->hydrate2(...); + $this->e2 = $this->extract2(...); + + $this->ready2 = true; + } finally { + $this->initializing2 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $e); + } + } + + if (\array_key_exists('profileCreated', $data)) { + try { + $value = $data['profileCreated']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $object->profileCreated = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $e); + } + } + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n4->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n4::class, $e); + } + + try { + $object->defaultArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $e); + } + } else { + $object->defaultArray = []; + } + + if (\array_key_exists('listArray', $data)) { + try { + $value = $this->n5->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n5::class, $e); + } + + try { + $object->listArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $e); + } + } else { + $object->listArray = []; + } + + if (\array_key_exists('iterableArray', $data)) { + try { + $value = $this->n6->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n6::class, $e); + } + + try { + $object->iterableArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $e); + } + } else { + $object->iterableArray = []; + } + + if (\array_key_exists('skillsHashMap', $data)) { + try { + $value = $data['skillsHashMap']; + + if ($this->ih1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hydrate2($item, $context) : $this->n8->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n7->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n7::class, $e); + } + + try { + $object->skillsHashMap = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $e); + } + } else { + $object->skillsHashMap = []; + } + + if (\array_key_exists('jsonArray', $data)) { + $value = $data['jsonArray']; + + try { + $object->jsonArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'jsonArray', $e); + } + } else { + $object->jsonArray = NULL; + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\NormalizerInBaseClassDefinedDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract1($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n4->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n4::class, $e); + } + + try { + $v_listArray = $this->n5->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n5::class, $e); + } + + try { + $v_iterableArray = $this->n6->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n6::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract2($item, $context) : $this->n8->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n7->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n7::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract1($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n4->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n4::class, $e); + } + + try { + $v_listArray = $this->n5->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n5::class, $e); + } + + try { + $v_iterableArray = $this->n6->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n6::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract2($item, $context) : $this->n8->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n7->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n7::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || ($this->ie0 ? $this->recursive1() : $this->n1 instanceof HydratorAwareNormalizer) + || $this->n4 instanceof HydratorAwareNormalizer + || $this->n5 instanceof HydratorAwareNormalizer + || $this->n6 instanceof HydratorAwareNormalizer + || ($this->ie1 ? false : $this->n7 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n2->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n2::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n3->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n3::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $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\ProfileCreatedWithNormalizer::class; + + try { + try { + $v_profileId = $this->n2->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n2::class, $e); + } + + try { + $v_email = $this->n3->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n3::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_profileId = $this->n2->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n2::class, $e); + } + + try { + $v_email = $this->n3->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n3::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = $this->n2 instanceof HydratorAwareNormalizer + || $this->n3 instanceof HydratorAwareNormalizer; + } + + public function hydrate2(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r2->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\Skill::class, 'name', $e); + } + } + + return $object; + } + + public function extract2(object $object, array $context): array + { + + + return [ + 'name' => $object->name, + ]; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php new file mode 100644 index 00000000..8448ef2a --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php @@ -0,0 +1,290 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->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\ParentDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\ParentDto::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\ParentDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } 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\ParentDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php new file mode 100644 index 00000000..eab4474c --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php @@ -0,0 +1,310 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public Closure $hp0; + public Closure $ep1; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return ($this->h0)($data, $context, $object); + } + + return ($this->h0)($data, $context); + } + + public function extract(object $object, array $context): array + { + return ($this->e0)($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->hp0 = Closure::bind(function (object $object, array $data, array $context): void { + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $e); + } + } + if (\array_key_exists('note', $data)) { + $value = $data['note']; + + try { + $object->note = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'note', $e); + } + } + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); + $this->h0 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r0->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\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); + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->ep1 = Closure::bind(function (object $object, array $context): array { + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n1::class, $e); + } + return [$v_email, $object->note]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); + $this->e0 = Closure::bind(function (object $object, array $context): array { + if ($this->tracked0) { + $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); + + 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); + + 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, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php new file mode 100644 index 00000000..a803ecd8 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php @@ -0,0 +1,275 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return ($this->h0)($data, $context, $object); + } + + return ($this->h0)($data, $context); + } + + public function extract(object $object, array $context): array + { + return ($this->e0)($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->h0 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r0->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->e0 = Closure::bind(function (object $object, array $context): array { + if ($this->tracked0) { + $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, + 'age' => $object->age, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php new file mode 100644 index 00000000..1554faec --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php @@ -0,0 +1,290 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->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\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\ProfileCreated::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\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } 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\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php new file mode 100644 index 00000000..d84c4318 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php @@ -0,0 +1,290 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'valueObject'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $e); + } + } + + if (\array_key_exists('valueObject', $data)) { + try { + $value = $this->n1->denormalize($data['valueObject'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + try { + $object->valueObject = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\ProfileCreatedWithInlineNormalizer::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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } 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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php new file mode 100644 index 00000000..a32e2774 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php @@ -0,0 +1,413 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ih0 = false; + public bool $ie0 = false; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n1; + public Normalizer $n2; + public bool $ready1 = false; + public bool $initializing1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileCreated'); + $this->ih0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, '626de5f0f972600f', Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, '626de5f0f972600f', Skip::Extract); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + if ($this->ih0 || $this->ie0) { $this->init1(); } + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + private function init1(): void + { + if ($this->ready1 || $this->initializing1) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing1 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class); + $this->r1 = $metadata->reflection; + $this->n1 = $this->normalizer($metadata, 'profileId'); + $this->n2 = $this->normalizer($metadata, 'email'); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + $this->tracked1 = $this->recursive1(); + + $this->ready1 = true; + } finally { + $this->initializing1 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate1($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + try { + $object->profileCreated = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract1($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = ($this->ie0 ? $this->recursive1() : $this->n0 instanceof HydratorAwareNormalizer); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n1->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n1::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n2->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n2::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $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\ProfileCreated::class; + + try { + try { + $v_profileId = $this->n1->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n1::class, $e); + } + + try { + $v_email = $this->n2->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n2::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_profileId = $this->n1->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n1::class, $e); + } + + try { + $v_email = $this->n2->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n2::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = $this->n1 instanceof HydratorAwareNormalizer + || $this->n2 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php new file mode 100644 index 00000000..ff039a1d --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php @@ -0,0 +1,290 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('id', $data)) { + try { + $value = $this->n0->denormalize($data['id'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } 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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php b/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php deleted file mode 100644 index 7f4f0c36..00000000 --- a/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php +++ /dev/null @@ -1,4305 +0,0 @@ - all middlewares except this one, its slot and the TransformMiddleware behind it */ - public array $others = []; - - /** @var array */ - public array $callStack = []; - - public Normalizer $n0; - public Normalizer $n1; - public bool $ready0 = false; - public bool $initializing0 = false; - public bool|null $recursive0 = null; - public Closure $h0; - public Closure $e0; - public ReflectionClass $r0; - public bool $tracked0 = true; - public Normalizer $n2; - public Normalizer $n3; - public bool $ready1 = false; - public bool $initializing1 = false; - public bool|null $recursive1 = null; - public Closure $h1; - public Closure $e1; - public ReflectionClass $r1; - public bool $tracked1 = true; - public Normalizer $n4; - public bool $ih0 = false; - public bool $ie0 = false; - public bool $ready2 = false; - public bool $initializing2 = false; - public bool|null $recursive2 = null; - public Closure $h2; - public Closure $e2; - public ReflectionClass $r2; - public bool $tracked2 = true; - public Normalizer $n5; - public bool $ih3 = false; - public bool $ie3 = false; - public bool $ready3 = false; - public bool $initializing3 = false; - public bool|null $recursive3 = null; - public Closure $h3; - public Closure $e3; - public ReflectionClass $r3; - public bool $tracked3 = true; - public Normalizer $n6; - public bool $ih2 = false; - public bool $ie2 = false; - public bool $ready4 = false; - public bool $initializing4 = false; - public bool|null $recursive4 = null; - public Closure $h4; - public Closure $e4; - public ReflectionClass $r4; - public bool $tracked4 = true; - public Normalizer $n7; - public bool $ih1 = false; - public bool $ie1 = false; - public bool $ready5 = false; - public bool $initializing5 = false; - public bool|null $recursive5 = null; - public Closure $h5; - public Closure $e5; - public ReflectionClass $r5; - public bool $tracked5 = true; - public Normalizer $n8; - public bool $ready6 = false; - public bool $initializing6 = false; - public bool|null $recursive6 = null; - public Closure $h6; - public Closure $e6; - public ReflectionClass $r6; - public bool $tracked6 = true; - public Normalizer $n9; - public bool $ih4 = false; - public bool $ie4 = false; - public ReflectionParameter $d0; - public bool $ready7 = false; - public bool $initializing7 = false; - public bool|null $recursive7 = null; - public Closure $h7; - public Closure $e7; - public ReflectionClass $r7; - public bool $tracked7 = true; - public bool $ready8 = false; - public bool $initializing8 = false; - public bool|null $recursive8 = null; - public Closure $h8; - public Closure $e8; - public ReflectionClass $r8; - public Normalizer $n10; - public Normalizer $n11; - public Normalizer $n12; - public Normalizer $n13; - public bool $ready9 = false; - public bool $initializing9 = false; - public bool|null $recursive9 = null; - public Closure $h9; - public Closure $e9; - public ReflectionClass $r9; - public bool $tracked9 = true; - public Normalizer $n14; - public Normalizer $n15; - public Normalizer $n16; - public Normalizer $n17; - public Normalizer $n18; - public bool $ready10 = false; - public bool $initializing10 = false; - public bool|null $recursive10 = null; - public Closure $h10; - public Closure $e10; - public ReflectionClass $r10; - public bool $tracked10 = true; - public Normalizer $n19; - public Normalizer $n20; - public Normalizer $n21; - public Normalizer $n22; - public Normalizer $n23; - public Normalizer $n24; - public bool $ready11 = false; - public bool $initializing11 = false; - public bool|null $recursive11 = null; - public Closure $h11; - public Closure $e11; - public ReflectionClass $r11; - public bool $tracked11 = true; - public Normalizer $n25; - public Normalizer $n26; - public bool $ih5 = false; - public bool $ie5 = false; - public Normalizer $n29; - public Normalizer $n30; - public Normalizer $n31; - public Normalizer $n32; - public ObjectNormalizer $n33; - public bool $ih6 = false; - public bool $ie6 = false; - public bool $ready12 = false; - public bool $initializing12 = false; - public bool|null $recursive12 = null; - public Closure $h12; - public Closure $e12; - public ReflectionClass $r12; - public bool $tracked12 = true; - public Normalizer $n27; - public Normalizer $n28; - public bool $ready13 = false; - public bool $initializing13 = false; - public bool|null $recursive13 = null; - public Closure $h13; - public Closure $e13; - public ReflectionClass $r13; - public bool $tracked13 = true; - public bool $ready14 = false; - public bool $initializing14 = false; - public bool|null $recursive14 = null; - public Closure $h14; - public Closure $e14; - public ReflectionClass $r14; - public Normalizer $n34; - public Normalizer $n35; - public bool $ready15 = false; - public bool $initializing15 = false; - public bool|null $recursive15 = null; - public Closure $h15; - public Closure $e15; - public ReflectionClass $r15; - public bool $tracked15 = true; - public Normalizer $n36; - public bool $ready16 = false; - public bool $initializing16 = false; - public bool|null $recursive16 = null; - public Closure $h16; - public Closure $e16; - public ReflectionClass $r16; - public bool $tracked16 = true; - public Normalizer $n37; - public Normalizer $n38; - public bool $ready17 = false; - public bool $initializing17 = false; - public bool|null $recursive17 = null; - public Closure $h17; - public Closure $e17; - public ReflectionClass $r17; - public bool $tracked17 = true; - public Normalizer $n39; - public bool $ready18 = false; - public bool $initializing18 = false; - public bool|null $recursive18 = null; - public Closure $h18; - public Closure $e18; - public ReflectionClass $r18; - public bool $tracked18 = true; - public Normalizer $n40; - public Normalizer $n41; - public bool $ready19 = false; - public bool $initializing19 = false; - public bool|null $recursive19 = null; - public Closure $h19; - public Closure $e19; - public ReflectionClass $r19; - public Closure $hp0; - public Closure $ep1; - public bool $tracked19 = true; - public Normalizer $n42; - public Normalizer $n43; - public bool $ready20 = false; - public bool $initializing20 = false; - public bool|null $recursive20 = null; - public Closure $h20; - public Closure $e20; - public ReflectionClass $r20; - public bool $tracked20 = true; - public Normalizer $n44; - public bool $ih7 = false; - public bool $ie7 = false; - public Normalizer $n45; - public ObjectNormalizer $n46; - public bool $ih8 = false; - public bool $ie8 = false; - public bool $ready21 = false; - public bool $initializing21 = false; - public bool|null $recursive21 = null; - public Closure $h21; - public Closure $e21; - public ReflectionClass $r21; - public bool $tracked21 = true; - public bool $ready22 = false; - public bool $initializing22 = false; - public bool|null $recursive22 = null; - public Closure $h22; - public Closure $e22; - public ReflectionClass $r22; - - public function __construct() - { - $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; - $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; - $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; - $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; - $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; - $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; - $this->h3 = function (array $data, array $context, object|null $object = null): object { $this->init3(); return ($this->h3)($data, $context, $object); }; - $this->e3 = function (object $object, array $context): array { $this->init3(); return ($this->e3)($object, $context); }; - $this->h4 = function (array $data, array $context, object|null $object = null): object { $this->init4(); return ($this->h4)($data, $context, $object); }; - $this->e4 = function (object $object, array $context): array { $this->init4(); return ($this->e4)($object, $context); }; - $this->h5 = function (array $data, array $context, object|null $object = null): object { $this->init5(); return ($this->h5)($data, $context, $object); }; - $this->e5 = function (object $object, array $context): array { $this->init5(); return ($this->e5)($object, $context); }; - $this->h6 = function (array $data, array $context, object|null $object = null): object { $this->init6(); return ($this->h6)($data, $context, $object); }; - $this->e6 = function (object $object, array $context): array { $this->init6(); return ($this->e6)($object, $context); }; - $this->h7 = function (array $data, array $context, object|null $object = null): object { $this->init7(); return ($this->h7)($data, $context, $object); }; - $this->e7 = function (object $object, array $context): array { $this->init7(); return ($this->e7)($object, $context); }; - $this->h8 = function (array $data, array $context, object|null $object = null): object { $this->init8(); return ($this->h8)($data, $context, $object); }; - $this->e8 = function (object $object, array $context): array { $this->init8(); return ($this->e8)($object, $context); }; - $this->h9 = function (array $data, array $context, object|null $object = null): object { $this->init9(); return ($this->h9)($data, $context, $object); }; - $this->e9 = function (object $object, array $context): array { $this->init9(); return ($this->e9)($object, $context); }; - $this->h10 = function (array $data, array $context, object|null $object = null): object { $this->init10(); return ($this->h10)($data, $context, $object); }; - $this->e10 = function (object $object, array $context): array { $this->init10(); return ($this->e10)($object, $context); }; - $this->h11 = function (array $data, array $context, object|null $object = null): object { $this->init11(); return ($this->h11)($data, $context, $object); }; - $this->e11 = function (object $object, array $context): array { $this->init11(); return ($this->e11)($object, $context); }; - $this->h12 = function (array $data, array $context, object|null $object = null): object { $this->init12(); return ($this->h12)($data, $context, $object); }; - $this->e12 = function (object $object, array $context): array { $this->init12(); return ($this->e12)($object, $context); }; - $this->h13 = function (array $data, array $context, object|null $object = null): object { $this->init13(); return ($this->h13)($data, $context, $object); }; - $this->e13 = function (object $object, array $context): array { $this->init13(); return ($this->e13)($object, $context); }; - $this->h14 = function (array $data, array $context, object|null $object = null): object { $this->init14(); return ($this->h14)($data, $context, $object); }; - $this->e14 = function (object $object, array $context): array { $this->init14(); return ($this->e14)($object, $context); }; - $this->h15 = function (array $data, array $context, object|null $object = null): object { $this->init15(); return ($this->h15)($data, $context, $object); }; - $this->e15 = function (object $object, array $context): array { $this->init15(); return ($this->e15)($object, $context); }; - $this->h16 = function (array $data, array $context, object|null $object = null): object { $this->init16(); return ($this->h16)($data, $context, $object); }; - $this->e16 = function (object $object, array $context): array { $this->init16(); return ($this->e16)($object, $context); }; - $this->h17 = function (array $data, array $context, object|null $object = null): object { $this->init17(); return ($this->h17)($data, $context, $object); }; - $this->e17 = function (object $object, array $context): array { $this->init17(); return ($this->e17)($object, $context); }; - $this->h18 = function (array $data, array $context, object|null $object = null): object { $this->init18(); return ($this->h18)($data, $context, $object); }; - $this->e18 = function (object $object, array $context): array { $this->init18(); return ($this->e18)($object, $context); }; - $this->h19 = function (array $data, array $context, object|null $object = null): object { $this->init19(); return ($this->h19)($data, $context, $object); }; - $this->e19 = function (object $object, array $context): array { $this->init19(); return ($this->e19)($object, $context); }; - $this->h20 = function (array $data, array $context, object|null $object = null): object { $this->init20(); return ($this->h20)($data, $context, $object); }; - $this->e20 = function (object $object, array $context): array { $this->init20(); return ($this->e20)($object, $context); }; - $this->h21 = function (array $data, array $context, object|null $object = null): object { $this->init21(); return ($this->h21)($data, $context, $object); }; - $this->e21 = function (object $object, array $context): array { $this->init21(); return ($this->e21)($object, $context); }; - $this->h22 = function (array $data, array $context, object|null $object = null): object { $this->init22(); return ($this->h22)($data, $context, $object); }; - $this->e22 = function (object $object, array $context): array { $this->init22(); return ($this->e22)($object, $context); }; - } - - public function setHydrator(StackHydrator $hydrator): void - { - $this->hydrator = $hydrator; - $this->others = []; - - foreach ($hydrator->middlewares() as $middleware) { - if ($middleware === $this || $middleware instanceof TransformMiddleware) { - continue; - } - - if ($middleware instanceof GeneratedMiddlewareSlot && $middleware->holds($this)) { - continue; - } - - $this->others[] = $middleware; - } - } - - /** Lets the GeneratedHydrator call the generated code directly, skipping the middleware stack. */ - public function compiledHydrator(ClassMetadata $metadata): Closure|null - { - if (!$this->exclusive($metadata, Skip::Hydrate)) { - return null; - } - - switch ($metadata->className) { - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class: - $this->init0(); - - return $this->fastHydrate0(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class: - $this->init1(); - - return $this->fastHydrate1(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class: - $this->init2(); - - return $this->fastHydrate2(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class: - $this->init3(); - - return $this->fastHydrate3(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class: - $this->init4(); - - return $this->fastHydrate4(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class: - $this->init5(); - - return $this->fastHydrate5(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class: - $this->init6(); - - return $this->fastHydrate6(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class: - $this->init7(); - - return $this->fastHydrate7(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class: - $this->init8(); - - return $this->fastHydrate8(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class: - $this->init9(); - - return $this->fastHydrate9(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class: - $this->init10(); - - return $this->fastHydrate10(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class: - $this->init11(); - - return $this->fastHydrate11(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class: - $this->init12(); - - return $this->fastHydrate12(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class: - $this->init13(); - - return $this->fastHydrate13(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class: - $this->init14(); - - return $this->fastHydrate14(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class: - $this->init15(); - - return $this->fastHydrate15(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class: - $this->init16(); - - return $this->fastHydrate16(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class: - $this->init17(); - - return $this->fastHydrate17(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class: - $this->init18(); - - return $this->fastHydrate18(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class: - $this->init19(); - - return $this->fastHydrate19(...); - case \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class: - $this->init20(); - - return $this->fastHydrate20(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class: - $this->init21(); - - return $this->fastHydrate21(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class: - $this->init22(); - - return $this->fastHydrate22(...); - default: - return null; - } - } - - public function compiledExtractor(ClassMetadata $metadata): Closure|null - { - if (!$this->exclusive($metadata, Skip::Extract)) { - return null; - } - - switch ($metadata->className) { - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class: - $this->init0(); - - return $this->extract0(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class: - $this->init1(); - - return $this->extract1(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class: - $this->init2(); - - return $this->extract2(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class: - $this->init3(); - - return $this->extract3(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class: - $this->init4(); - - return $this->extract4(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class: - $this->init5(); - - return $this->extract5(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class: - $this->init6(); - - return $this->extract6(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class: - $this->init7(); - - return $this->extract7(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class: - $this->init8(); - - return $this->e8; - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class: - $this->init9(); - - return $this->extract9(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class: - $this->init10(); - - return $this->extract10(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class: - $this->init11(); - - return $this->extract11(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class: - $this->init12(); - - return $this->extract12(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class: - $this->init13(); - - return $this->extract13(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class: - $this->init14(); - - return $this->extract14(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class: - $this->init15(); - - return $this->extract15(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class: - $this->init16(); - - return $this->extract16(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class: - $this->init17(); - - return $this->extract17(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class: - $this->init18(); - - return $this->e18; - case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class: - $this->init19(); - - return $this->e19; - case \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class: - $this->init20(); - - return $this->extract20(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class: - $this->init21(); - - return $this->extract21(...); - case \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class: - $this->init22(); - - return $this->extract22(...); - default: - return null; - } - } - - /** Classes without generated code are left to the TransformMiddleware directly. */ - public function skip(ClassMetadata $metadata): Skip - { - return match ($metadata->className) { - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, - \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, - \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, - => Skip::None, - default => Skip::Both, - }; - } - - public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object - { - $object = null; - $populate = $context; - - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($populate[Hydrator::OBJECT_TO_POPULATE]); - } - - return match ($metadata->className) { - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class => $this->ready0 ? $this->hydrate0($data, $populate, $object) : ($this->h0)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class => $this->ready1 ? $this->hydrate1($data, $populate, $object) : ($this->h1)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class => $this->ready2 ? $this->hydrate2($data, $populate, $object) : ($this->h2)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class => $this->ready3 ? $this->hydrate3($data, $populate, $object) : ($this->h3)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class => $this->ready4 ? $this->hydrate4($data, $populate, $object) : ($this->h4)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class => $this->ready5 ? $this->hydrate5($data, $populate, $object) : ($this->h5)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class => $this->ready6 ? $this->hydrate6($data, $populate, $object) : ($this->h6)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class => $this->ready7 ? $this->hydrate7($data, $populate, $object) : ($this->h7)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class => ($this->h8)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class => $this->ready9 ? $this->hydrate9($data, $populate, $object) : ($this->h9)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class => $this->ready10 ? $this->hydrate10($data, $populate, $object) : ($this->h10)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class => $this->ready11 ? $this->hydrate11($data, $populate, $object) : ($this->h11)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class => $this->ready12 ? $this->hydrate12($data, $populate, $object) : ($this->h12)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class => $this->ready13 ? $this->hydrate13($data, $populate, $object) : ($this->h13)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class => $this->ready14 ? $this->hydrate14($data, $populate, $object) : ($this->h14)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class => $this->ready15 ? $this->hydrate15($data, $populate, $object) : ($this->h15)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class => $this->ready16 ? $this->hydrate16($data, $populate, $object) : ($this->h16)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class => $this->ready17 ? $this->hydrate17($data, $populate, $object) : ($this->h17)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class => ($this->h18)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class => ($this->h19)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class => $this->ready20 ? $this->hydrate20($data, $populate, $object) : ($this->h20)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class => $this->ready21 ? $this->hydrate21($data, $populate, $object) : ($this->h21)($data, $populate, $object), - \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class => $this->ready22 ? $this->hydrate22($data, $populate, $object) : ($this->h22)($data, $populate, $object), - default => $stack->next()->hydrate($metadata, $data, $context, $stack), - }; - } - - public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array - { - return match ($metadata->className) { - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class => $this->ready0 ? $this->extract0($object, $context) : ($this->e0)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class => $this->ready1 ? $this->extract1($object, $context) : ($this->e1)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class => $this->ready2 ? $this->extract2($object, $context) : ($this->e2)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class => $this->ready3 ? $this->extract3($object, $context) : ($this->e3)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class => $this->ready4 ? $this->extract4($object, $context) : ($this->e4)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class => $this->ready5 ? $this->extract5($object, $context) : ($this->e5)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class => $this->ready6 ? $this->extract6($object, $context) : ($this->e6)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class => $this->ready7 ? $this->extract7($object, $context) : ($this->e7)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class => ($this->e8)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class => $this->ready9 ? $this->extract9($object, $context) : ($this->e9)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class => $this->ready10 ? $this->extract10($object, $context) : ($this->e10)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class => $this->ready11 ? $this->extract11($object, $context) : ($this->e11)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class => $this->ready12 ? $this->extract12($object, $context) : ($this->e12)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class => $this->ready13 ? $this->extract13($object, $context) : ($this->e13)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class => $this->ready14 ? $this->extract14($object, $context) : ($this->e14)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class => $this->ready15 ? $this->extract15($object, $context) : ($this->e15)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class => $this->ready16 ? $this->extract16($object, $context) : ($this->e16)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class => $this->ready17 ? $this->extract17($object, $context) : ($this->e17)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class => ($this->e18)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class => ($this->e19)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class => $this->ready20 ? $this->extract20($object, $context) : ($this->e20)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class => $this->ready21 ? $this->extract21($object, $context) : ($this->e21)($object, $context), - \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class => $this->ready22 ? $this->extract22($object, $context) : ($this->e22)($object, $context), - default => $stack->next()->extract($metadata, $object, $context, $stack), - }; - } - - private function init0(): void - { - if ($this->ready0 || $this->initializing0) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing0 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, '626de5f0f972600f'); - $this->r0 = $metadata->reflection; - $this->n0 = $this->normalizer($metadata, 'profileId'); - $this->n1 = $this->normalizer($metadata, 'email'); - $this->h0 = $this->hydrate0(...); - $this->e0 = $this->extract0(...); - $this->tracked0 = $this->recursive0(); - - $this->ready0 = true; - } finally { - $this->initializing0 = false; - } - } - - private function init1(): void - { - if ($this->ready1 || $this->initializing1) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing1 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'eef45a73100e0e6c'); - $this->r1 = $metadata->reflection; - $this->n2 = $this->normalizer($metadata, 'profileId'); - $this->n3 = $this->normalizer($metadata, 'email'); - $this->h1 = $this->hydrate1(...); - $this->e1 = $this->extract1(...); - $this->tracked1 = $this->recursive1(); - - $this->ready1 = true; - } finally { - $this->initializing1 = false; - } - } - - private function init2(): void - { - if ($this->ready2 || $this->initializing2) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing2 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'b4cf92da1c60c973'); - $this->r2 = $metadata->reflection; - $this->n4 = $this->normalizer($metadata, 'profileCreated'); - $this->ih0 = $this->inlineObject($this->n4, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Skip::Hydrate); - $this->ie0 = $this->inlineObject($this->n4, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Skip::Extract); - $this->h2 = $this->hydrate2(...); - $this->e2 = $this->extract2(...); - $this->init0(); - $this->tracked2 = $this->recursive2(); - - $this->ready2 = true; - } finally { - $this->initializing2 = false; - } - } - - private function init3(): void - { - if ($this->ready3 || $this->initializing3) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing3 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, '0903da95af9a7396'); - $this->r3 = $metadata->reflection; - $this->n5 = $this->normalizer($metadata, 'to'); - $this->ih3 = $this->inlineObject($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Skip::Hydrate); - $this->ie3 = $this->inlineObject($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Skip::Extract); - $this->h3 = $this->hydrate3(...); - $this->e3 = $this->extract3(...); - $this->init4(); - $this->tracked3 = $this->recursive3(); - - $this->ready3 = true; - } finally { - $this->initializing3 = false; - } - } - - private function init4(): void - { - if ($this->ready4 || $this->initializing4) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing4 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, '0903da95af9a7396'); - $this->r4 = $metadata->reflection; - $this->n6 = $this->normalizer($metadata, 'to'); - $this->ih2 = $this->inlineObject($this->n6, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Skip::Hydrate); - $this->ie2 = $this->inlineObject($this->n6, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Skip::Extract); - $this->h4 = $this->hydrate4(...); - $this->e4 = $this->extract4(...); - $this->init5(); - $this->tracked4 = $this->recursive4(); - - $this->ready4 = true; - } finally { - $this->initializing4 = false; - } - } - - private function init5(): void - { - if ($this->ready5 || $this->initializing5) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing5 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, '0903da95af9a7396'); - $this->r5 = $metadata->reflection; - $this->n7 = $this->normalizer($metadata, 'to'); - $this->ih1 = $this->inlineObject($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Skip::Hydrate); - $this->ie1 = $this->inlineObject($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Skip::Extract); - $this->h5 = $this->hydrate5(...); - $this->e5 = $this->extract5(...); - $this->init3(); - $this->tracked5 = $this->recursive5(); - - $this->ready5 = true; - } finally { - $this->initializing5 = false; - } - } - - private function init6(): void - { - if ($this->ready6 || $this->initializing6) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing6 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, '3208e9650968f0e3'); - $this->r6 = $metadata->reflection; - $this->n8 = $this->normalizer($metadata, 'value'); - $this->h6 = $this->hydrate6(...); - $this->e6 = $this->extract6(...); - $this->tracked6 = $this->recursive6(); - - $this->ready6 = true; - } finally { - $this->initializing6 = false; - } - } - - private function init7(): void - { - if ($this->ready7 || $this->initializing7) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing7 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, '83cd14cb31cfabdc'); - $this->r7 = $metadata->reflection; - $this->d0 = $this->promotedDefault($metadata, 'email'); - $this->n9 = $this->normalizer($metadata, 'email'); - $this->ih4 = $this->inlineObject($this->n9, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Skip::Hydrate); - $this->ie4 = $this->inlineObject($this->n9, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Skip::Extract); - $this->h7 = $this->hydrate7(...); - $this->e7 = $this->extract7(...); - $this->init8(); - $this->tracked7 = $this->recursive7(); - - $this->ready7 = true; - } finally { - $this->initializing7 = false; - } - } - - private function init8(): void - { - if ($this->ready8 || $this->initializing8) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing8 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, '136535d19b213863'); - $this->r8 = $metadata->reflection; - $this->h8 = Closure::bind(function (array $data, array $context, object|null $object = null): object { - $object ??= $this->r8->newInstanceWithoutConstructor(); - - if (\array_key_exists('value', $data)) { - $value = $data['value']; - - try { - $object->value = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, 'value', $e); - } - } - - return $object; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); - $this->e8 = Closure::bind(function (object $object, array $context): array { - - - return [ - 'value' => $object->value, - ]; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); - - $this->ready8 = true; - } finally { - $this->initializing8 = false; - } - } - - private function init9(): void - { - if ($this->ready9 || $this->initializing9) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing9 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, '684eb1c508f4ac36'); - $this->r9 = $metadata->reflection; - $this->n10 = $this->normalizer($metadata, 'status'); - $this->n11 = $this->normalizer($metadata, 'dateTimeImmutable'); - $this->n12 = $this->normalizer($metadata, 'dateTime'); - $this->n13 = $this->normalizer($metadata, 'dateTimeZone'); - $this->h9 = $this->hydrate9(...); - $this->e9 = $this->extract9(...); - $this->tracked9 = $this->recursive9(); - - $this->ready9 = true; - } finally { - $this->initializing9 = false; - } - } - - private function init10(): void - { - if ($this->ready10 || $this->initializing10) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing10 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, '9a9ddf85fffebd93'); - $this->r10 = $metadata->reflection; - $this->n14 = $this->normalizer($metadata, 'status'); - $this->n15 = $this->normalizer($metadata, 'dateTimeImmutable'); - $this->n16 = $this->normalizer($metadata, 'dateTime'); - $this->n17 = $this->normalizer($metadata, 'dateTimeZone'); - $this->n18 = $this->normalizer($metadata, 'profileId'); - $this->h10 = $this->hydrate10(...); - $this->e10 = $this->extract10(...); - $this->tracked10 = $this->recursive10(); - - $this->ready10 = true; - } finally { - $this->initializing10 = false; - } - } - - private function init11(): void - { - if ($this->ready11 || $this->initializing11) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing11 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, '05328b19c85adc56'); - $this->r11 = $metadata->reflection; - $this->n19 = $this->normalizer($metadata, 'defaultArray'); - $this->n20 = $this->normalizer($metadata, 'listArray'); - $this->n21 = $this->normalizer($metadata, 'iterableArray'); - $this->n22 = $this->normalizer($metadata, 'hashMap'); - $this->n23 = $this->normalizer($metadata, 'nested'); - $this->n24 = $this->normalizer($metadata, 'shapeArray'); - $this->h11 = $this->hydrate11(...); - $this->e11 = $this->extract11(...); - $this->tracked11 = $this->recursive11(); - - $this->ready11 = true; - } finally { - $this->initializing11 = false; - } - } - - private function init12(): void - { - if ($this->ready12 || $this->initializing12) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing12 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'fc8af35d058badeb'); - $this->r12 = $metadata->reflection; - $this->n25 = $this->normalizer($metadata, 'status'); - $this->n26 = $this->normalizer($metadata, 'profileCreated'); - $this->ih5 = $this->inlineObject($this->n26, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Skip::Hydrate); - $this->ie5 = $this->inlineObject($this->n26, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Skip::Extract); - $this->n29 = $this->normalizer($metadata, 'defaultArray'); - $this->n30 = $this->normalizer($metadata, 'listArray'); - $this->n31 = $this->normalizer($metadata, 'iterableArray'); - $this->n32 = $this->normalizer($metadata, 'skillsHashMap'); - $this->ih6 = $this->inlineArray($this->n32, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Skip::Hydrate); - $this->ie6 = $this->inlineArray($this->n32, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Skip::Extract); - if ($this->ih6 || $this->ie6) { $this->n33 = $this->n32->innerNormalizer(); } - $this->h12 = $this->hydrate12(...); - $this->e12 = $this->extract12(...); - $this->init13(); - $this->init14(); - $this->tracked12 = $this->recursive12(); - - $this->ready12 = true; - } finally { - $this->initializing12 = false; - } - } - - private function init13(): void - { - if ($this->ready13 || $this->initializing13) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing13 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, '626de5f0f972600f'); - $this->r13 = $metadata->reflection; - $this->n27 = $this->normalizer($metadata, 'profileId'); - $this->n28 = $this->normalizer($metadata, 'email'); - $this->h13 = $this->hydrate13(...); - $this->e13 = $this->extract13(...); - $this->tracked13 = $this->recursive13(); - - $this->ready13 = true; - } finally { - $this->initializing13 = false; - } - } - - private function init14(): void - { - if ($this->ready14 || $this->initializing14) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing14 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, 'f6938c7b12413f3a'); - $this->r14 = $metadata->reflection; - $this->h14 = $this->hydrate14(...); - $this->e14 = $this->extract14(...); - - $this->ready14 = true; - } finally { - $this->initializing14 = false; - } - } - - private function init15(): void - { - if ($this->ready15 || $this->initializing15) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing15 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, '626de5f0f972600f'); - $this->r15 = $metadata->reflection; - $this->n34 = $this->normalizer($metadata, 'profileId'); - $this->n35 = $this->normalizer($metadata, 'email'); - $this->h15 = $this->hydrate15(...); - $this->e15 = $this->extract15(...); - $this->tracked15 = $this->recursive15(); - - $this->ready15 = true; - } finally { - $this->initializing15 = false; - } - } - - private function init16(): void - { - if ($this->ready16 || $this->initializing16) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing16 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, '051d6ccde451cf68'); - $this->r16 = $metadata->reflection; - $this->n36 = $this->normalizer($metadata, 'email'); - $this->h16 = $this->hydrate16(...); - $this->e16 = $this->extract16(...); - $this->tracked16 = $this->recursive16(); - - $this->ready16 = true; - } finally { - $this->initializing16 = false; - } - } - - private function init17(): void - { - if ($this->ready17 || $this->initializing17) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing17 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, '22ad1fb2b9cc3187'); - $this->r17 = $metadata->reflection; - $this->n37 = $this->normalizer($metadata, 'profileId'); - $this->n38 = $this->normalizer($metadata, 'valueObject'); - $this->h17 = $this->hydrate17(...); - $this->e17 = $this->extract17(...); - $this->tracked17 = $this->recursive17(); - - $this->ready17 = true; - } finally { - $this->initializing17 = false; - } - } - - private function init18(): void - { - if ($this->ready18 || $this->initializing18) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing18 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, '7d2a870342b40a8a'); - $this->r18 = $metadata->reflection; - $this->n39 = $this->normalizer($metadata, 'profileId'); - $this->h18 = Closure::bind(function (array $data, array $context, object|null $object = null): object { - $object ??= $this->r18->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n39->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::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->e18 = Closure::bind(function (object $object, array $context): array { - if ($this->tracked18) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class; - - try { - try { - $v_profileId = $this->n39->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'name' => $object->name, - 'age' => $object->age, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n39->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'name' => $object->name, - 'age' => $object->age, - ]; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); - $this->tracked18 = $this->recursive18(); - - $this->ready18 = true; - } finally { - $this->initializing18 = false; - } - } - - private function init19(): void - { - if ($this->ready19 || $this->initializing19) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing19 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'f5e611c6ad0224f7'); - $this->r19 = $metadata->reflection; - $this->n40 = $this->normalizer($metadata, 'profileId'); - $this->n41 = $this->normalizer($metadata, 'email'); - $this->hp0 = Closure::bind(function (object $object, array $data, array $context): void { - if (\array_key_exists('email', $data)) { - try { - $value = $this->n41->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n41::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $e); - } - } - if (\array_key_exists('note', $data)) { - $value = $data['note']; - - try { - $object->note = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'note', $e); - } - } - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); - $this->h19 = Closure::bind(function (array $data, array $context, object|null $object = null): object { - $object ??= $this->r19->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n40->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::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); - - return $object; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); - $this->ep1 = Closure::bind(function (object $object, array $context): array { - try { - $v_email = $this->n41->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n41::class, $e); - } - return [$v_email, $object->note]; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); - $this->e19 = Closure::bind(function (object $object, array $context): array { - if ($this->tracked19) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class; - - try { - [$v_email, $v_note] = ($this->ep1)($object, $context); - - try { - $v_profileId = $this->n40->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'name' => $object->name, - 'email' => $v_email, - 'note' => $v_note, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - [$v_email, $v_note] = ($this->ep1)($object, $context); - - try { - $v_profileId = $this->n40->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'name' => $object->name, - 'email' => $v_email, - 'note' => $v_note, - ]; - }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); - $this->tracked19 = $this->recursive19(); - - $this->ready19 = true; - } finally { - $this->initializing19 = false; - } - } - - private function init20(): void - { - if ($this->ready20 || $this->initializing20) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing20 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'e3b5c5aad0ddd5a3'); - $this->r20 = $metadata->reflection; - $this->n42 = $this->normalizer($metadata, 'profileId'); - $this->n43 = $this->normalizer($metadata, 'email'); - $this->h20 = $this->hydrate20(...); - $this->e20 = $this->extract20(...); - $this->tracked20 = $this->recursive20(); - - $this->ready20 = true; - } finally { - $this->initializing20 = false; - } - } - - private function init21(): void - { - if ($this->ready21 || $this->initializing21) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing21 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, '3e51d2f79ff7bad2'); - $this->r21 = $metadata->reflection; - $this->n44 = $this->normalizer($metadata, 'child'); - $this->ih7 = $this->inlineObject($this->n44, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Hydrate); - $this->ie7 = $this->inlineObject($this->n44, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Extract); - $this->n45 = $this->normalizer($metadata, 'items'); - $this->ih8 = $this->inlineArray($this->n45, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Hydrate); - $this->ie8 = $this->inlineArray($this->n45, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Extract); - if ($this->ih8 || $this->ie8) { $this->n46 = $this->n45->innerNormalizer(); } - $this->h21 = $this->hydrate21(...); - $this->e21 = $this->extract21(...); - $this->init22(); - $this->tracked21 = $this->recursive21(); - - $this->ready21 = true; - } finally { - $this->initializing21 = false; - } - } - - private function init22(): void - { - if ($this->ready22 || $this->initializing22) { - return; - } - - // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving - // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. - $this->initializing22 = true; - - try { - $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'f6938c7b12413f3a'); - $this->r22 = $metadata->reflection; - $this->h22 = $this->hydrate22(...); - $this->e22 = $this->extract22(...); - - $this->ready22 = true; - } finally { - $this->initializing22 = false; - } - } - - public function fastHydrate0(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate0($data, $context, $object); - } - - return $this->hydrate0($data, $context); - } - - public function hydrate0(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r0->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\ProfileCreated::class, 'profileId', $this->n0::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n1->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $e); - } - } - - return $object; - } - - public function extract0(object $object, array $context): array - { - if ($this->tracked0) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::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\ProfileCreated::class, 'profileId', $this->n0::class, $e); - } - - try { - $v_email = $this->n1->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$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\ProfileCreated::class, 'profileId', $this->n0::class, $e); - } - - try { - $v_email = $this->n1->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } - - private function recursive0(): bool - { - if ($this->recursive0 !== null) { - return $this->recursive0; - } - - $this->recursive0 = true; - - return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer - || $this->n1 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate1(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate1($data, $context, $object); - } - - return $this->hydrate1($data, $context); - } - - public function hydrate1(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r1->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n2->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n3->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $e); - } - } - - return $object; - } - - public function extract1(object $object, array $context): array - { - if ($this->tracked1) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class; - - try { - try { - $v_profileId = $this->n2->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); - } - - try { - $v_email = $this->n3->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n2->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); - } - - try { - $v_email = $this->n3->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } - - private function recursive1(): bool - { - if ($this->recursive1 !== null) { - return $this->recursive1; - } - - $this->recursive1 = true; - - return $this->recursive1 = $this->n2 instanceof HydratorAwareNormalizer - || $this->n3 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate2(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate2($data, $context, $object); - } - - return $this->hydrate2($data, $context); - } - - public function hydrate2(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r2->newInstanceWithoutConstructor(); - - if (\array_key_exists('event', $data)) { - try { - $value = $data['event']; - - if ($this->ih0 && \is_array($value)) { - $value = $this->hydrate0($value, $context); - } else { - $value = $this->n4->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); - } - - try { - $object->profileCreated = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $e); - } - } - - return $object; - } - - public function extract2(object $object, array $context): array - { - if ($this->tracked2) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; - - try { - try { - $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract0($value, $context) : $this->n4->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); - } - - return [ - 'event' => $v_profileCreated, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->profileCreated; - $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract0($value, $context) : $this->n4->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); - } - - return [ - 'event' => $v_profileCreated, - ]; - } - - private function recursive2(): bool - { - if ($this->recursive2 !== null) { - return $this->recursive2; - } - - $this->recursive2 = true; - - return $this->recursive2 = ($this->ie0 ? $this->recursive0() : $this->n4 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate3(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate3($data, $context, $object); - } - - return $this->hydrate3($data, $context); - } - - public function hydrate3(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r3->newInstanceWithoutConstructor(); - - if (\array_key_exists('to', $data)) { - try { - $value = $data['to']; - - if ($this->ih3 && \is_array($value)) { - $value = $this->hydrate4($value, $context); - } else { - $value = $this->n5->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); - } - - try { - $object->to = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $e); - } - } - - return $object; - } - - public function extract3(object $object, array $context): array - { - if ($this->tracked3) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; - - try { - try { - $value = $object->to; - $v_to = $this->ie3 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract4($value, $context) : $this->n5->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->to; - $v_to = $this->ie3 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract4($value, $context) : $this->n5->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } - - private function recursive3(): bool - { - if ($this->recursive3 !== null) { - return $this->recursive3; - } - - $this->recursive3 = true; - - return $this->recursive3 = ($this->ie3 ? $this->recursive4() : $this->n5 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate4(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate4($data, $context, $object); - } - - return $this->hydrate4($data, $context); - } - - public function hydrate4(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r4->newInstanceWithoutConstructor(); - - if (\array_key_exists('to', $data)) { - try { - $value = $data['to']; - - if ($this->ih2 && \is_array($value)) { - $value = $this->hydrate5($value, $context); - } else { - $value = $this->n6->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); - } - - try { - $object->to = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $e); - } - } - - return $object; - } - - public function extract4(object $object, array $context): array - { - if ($this->tracked4) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; - - try { - try { - $value = $object->to; - $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract5($value, $context) : $this->n6->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->to; - $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract5($value, $context) : $this->n6->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } - - private function recursive4(): bool - { - if ($this->recursive4 !== null) { - return $this->recursive4; - } - - $this->recursive4 = true; - - return $this->recursive4 = ($this->ie2 ? $this->recursive5() : $this->n6 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate5(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate5($data, $context, $object); - } - - return $this->hydrate5($data, $context); - } - - public function hydrate5(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r5->newInstanceWithoutConstructor(); - - if (\array_key_exists('to', $data)) { - try { - $value = $data['to']; - - if ($this->ih1 && \is_array($value)) { - $value = $this->hydrate3($value, $context); - } else { - $value = $this->n7->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); - } - - try { - $object->to = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $e); - } - } - - return $object; - } - - public function extract5(object $object, array $context): array - { - if ($this->tracked5) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; - - try { - try { - $value = $object->to; - $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract3($value, $context) : $this->n7->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->to; - $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract3($value, $context) : $this->n7->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); - } - - return [ - 'to' => $v_to, - ]; - } - - private function recursive5(): bool - { - if ($this->recursive5 !== null) { - return $this->recursive5; - } - - $this->recursive5 = true; - - return $this->recursive5 = ($this->ie1 ? $this->recursive3() : $this->n7 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate6(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate6($data, $context, $object); - } - - return $this->hydrate6($data, $context); - } - - public function hydrate6(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r6->newInstanceWithoutConstructor(); - - if (\array_key_exists('value', $data)) { - try { - $value = $this->n8->denormalize($data['value'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); - } - - try { - $object->value = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $e); - } - } - - return $object; - } - - public function extract6(object $object, array $context): array - { - if ($this->tracked6) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class; - - try { - try { - $v_value = $this->n8->normalize($object->value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); - } - - return [ - 'value' => $v_value, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_value = $this->n8->normalize($object->value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); - } - - return [ - 'value' => $v_value, - ]; - } - - private function recursive6(): bool - { - if ($this->recursive6 !== null) { - return $this->recursive6; - } - - $this->recursive6 = true; - - return $this->recursive6 = $this->n8 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate7(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate7($data, $context, $object); - } - - return $this->hydrate7($data, $context); - } - - public function hydrate7(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r7->newInstanceWithoutConstructor(); - - if (\array_key_exists('admin', $data)) { - $value = $data['admin']; - - try { - $object->admin = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'admin', $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\DefaultDto::class, 'name', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $data['email']; - - if ($this->ih4 && \is_array($value)) { - $value = ($this->h8)($value, $context); - } else { - $value = $this->n9->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $e); - } - } else { - $object->email = $this->d0->getDefaultValue(); - } - - return $object; - } - - public function extract7(object $object, array $context): array - { - if ($this->tracked7) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; - - try { - try { - $value = $object->email; - $v_email = $this->ie4 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e8)($value, $context) : $this->n9->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); - } - - return [ - 'admin' => $object->admin, - 'name' => $object->name, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->email; - $v_email = $this->ie4 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e8)($value, $context) : $this->n9->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); - } - - return [ - 'admin' => $object->admin, - 'name' => $object->name, - 'email' => $v_email, - ]; - } - - private function recursive7(): bool - { - if ($this->recursive7 !== null) { - return $this->recursive7; - } - - $this->recursive7 = true; - - return $this->recursive7 = ($this->ie4 ? false : $this->n9 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate8(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return ($this->h8)($data, $context, $object); - } - - return ($this->h8)($data, $context); - } - - public function fastHydrate9(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate9($data, $context, $object); - } - - return $this->hydrate9($data, $context); - } - - public function hydrate9(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r9->newInstanceWithoutConstructor(); - - if (\array_key_exists('status', $data)) { - try { - $value = $this->n10->denormalize($data['status'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); - } - - try { - $object->status = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $e); - } - } - - if (\array_key_exists('dateTimeImmutable', $data)) { - try { - $value = $this->n11->denormalize($data['dateTimeImmutable'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); - } - - try { - $object->dateTimeImmutable = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $e); - } - } - - if (\array_key_exists('dateTime', $data)) { - try { - $value = $this->n12->denormalize($data['dateTime'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); - } - - try { - $object->dateTime = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $e); - } - } - - if (\array_key_exists('dateTimeZone', $data)) { - try { - $value = $this->n13->denormalize($data['dateTimeZone'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); - } - - try { - $object->dateTimeZone = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $e); - } - } - - if (\array_key_exists('array', $data)) { - $value = $data['array']; - - try { - $object->array = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'array', $e); - } - } - - return $object; - } - - public function extract9(object $object, array $context): array - { - if ($this->tracked9) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class; - - try { - try { - $v_status = $this->n10->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); - } - - try { - $v_dateTimeImmutable = $this->n11->normalize($object->dateTimeImmutable, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); - } - - try { - $v_dateTime = $this->n12->normalize($object->dateTime, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); - } - - try { - $v_dateTimeZone = $this->n13->normalize($object->dateTimeZone, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); - } - - return [ - 'status' => $v_status, - 'dateTimeImmutable' => $v_dateTimeImmutable, - 'dateTime' => $v_dateTime, - 'dateTimeZone' => $v_dateTimeZone, - 'array' => $object->array, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_status = $this->n10->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); - } - - try { - $v_dateTimeImmutable = $this->n11->normalize($object->dateTimeImmutable, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); - } - - try { - $v_dateTime = $this->n12->normalize($object->dateTime, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); - } - - try { - $v_dateTimeZone = $this->n13->normalize($object->dateTimeZone, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); - } - - return [ - 'status' => $v_status, - 'dateTimeImmutable' => $v_dateTimeImmutable, - 'dateTime' => $v_dateTime, - 'dateTimeZone' => $v_dateTimeZone, - 'array' => $object->array, - ]; - } - - private function recursive9(): bool - { - if ($this->recursive9 !== null) { - return $this->recursive9; - } - - $this->recursive9 = true; - - return $this->recursive9 = $this->n10 instanceof HydratorAwareNormalizer - || $this->n11 instanceof HydratorAwareNormalizer - || $this->n12 instanceof HydratorAwareNormalizer - || $this->n13 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate10(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate10($data, $context, $object); - } - - return $this->hydrate10($data, $context); - } - - public function hydrate10(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r10->newInstanceWithoutConstructor(); - - if (\array_key_exists('status', $data)) { - try { - $value = $this->n14->denormalize($data['status'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); - } - - try { - $object->status = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $e); - } - } - - if (\array_key_exists('dateTimeImmutable', $data)) { - try { - $value = $this->n15->denormalize($data['dateTimeImmutable'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); - } - - try { - $object->dateTimeImmutable = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $e); - } - } - - if (\array_key_exists('dateTime', $data)) { - try { - $value = $this->n16->denormalize($data['dateTime'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); - } - - try { - $object->dateTime = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $e); - } - } else { - $object->dateTime = NULL; - } - - if (\array_key_exists('dateTimeZone', $data)) { - try { - $value = $this->n17->denormalize($data['dateTimeZone'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); - } - - try { - $object->dateTimeZone = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $e); - } - } else { - $object->dateTimeZone = NULL; - } - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n18->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $e); - } - } else { - $object->profileId = NULL; - } - - return $object; - } - - public function extract10(object $object, array $context): array - { - if ($this->tracked10) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class; - - try { - try { - $v_status = $this->n14->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); - } - - try { - $v_dateTimeImmutable = $this->n15->normalize($object->dateTimeImmutable, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); - } - - try { - $v_dateTime = $this->n16->normalize($object->dateTime, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); - } - - try { - $v_dateTimeZone = $this->n17->normalize($object->dateTimeZone, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); - } - - try { - $v_profileId = $this->n18->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); - } - - return [ - 'status' => $v_status, - 'dateTimeImmutable' => $v_dateTimeImmutable, - 'dateTime' => $v_dateTime, - 'dateTimeZone' => $v_dateTimeZone, - 'profileId' => $v_profileId, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_status = $this->n14->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); - } - - try { - $v_dateTimeImmutable = $this->n15->normalize($object->dateTimeImmutable, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); - } - - try { - $v_dateTime = $this->n16->normalize($object->dateTime, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); - } - - try { - $v_dateTimeZone = $this->n17->normalize($object->dateTimeZone, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); - } - - try { - $v_profileId = $this->n18->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); - } - - return [ - 'status' => $v_status, - 'dateTimeImmutable' => $v_dateTimeImmutable, - 'dateTime' => $v_dateTime, - 'dateTimeZone' => $v_dateTimeZone, - 'profileId' => $v_profileId, - ]; - } - - private function recursive10(): bool - { - if ($this->recursive10 !== null) { - return $this->recursive10; - } - - $this->recursive10 = true; - - return $this->recursive10 = $this->n14 instanceof HydratorAwareNormalizer - || $this->n15 instanceof HydratorAwareNormalizer - || $this->n16 instanceof HydratorAwareNormalizer - || $this->n17 instanceof HydratorAwareNormalizer - || $this->n18 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate11(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate11($data, $context, $object); - } - - return $this->hydrate11($data, $context); - } - - public function hydrate11(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r11->newInstanceWithoutConstructor(); - - if (\array_key_exists('defaultArray', $data)) { - try { - $value = $this->n19->denormalize($data['defaultArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); - } - - try { - $object->defaultArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $e); - } - } else { - $object->defaultArray = []; - } - - if (\array_key_exists('listArray', $data)) { - try { - $value = $this->n20->denormalize($data['listArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); - } - - try { - $object->listArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $e); - } - } else { - $object->listArray = []; - } - - if (\array_key_exists('iterableArray', $data)) { - try { - $value = $this->n21->denormalize($data['iterableArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); - } - - try { - $object->iterableArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $e); - } - } else { - $object->iterableArray = []; - } - - if (\array_key_exists('hashMap', $data)) { - try { - $value = $this->n22->denormalize($data['hashMap'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); - } - - try { - $object->hashMap = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $e); - } - } else { - $object->hashMap = []; - } - - if (\array_key_exists('nested', $data)) { - try { - $value = $this->n23->denormalize($data['nested'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); - } - - try { - $object->nested = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $e); - } - } else { - $object->nested = []; - } - - if (\array_key_exists('jsonArray', $data)) { - $value = $data['jsonArray']; - - try { - $object->jsonArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'jsonArray', $e); - } - } else { - $object->jsonArray = NULL; - } - - if (\array_key_exists('shapeArray', $data)) { - try { - $value = $this->n24->denormalize($data['shapeArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); - } - - try { - $object->shapeArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $e); - } - } else { - $object->shapeArray = NULL; - } - - return $object; - } - - public function extract11(object $object, array $context): array - { - if ($this->tracked11) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class; - - try { - try { - $v_defaultArray = $this->n19->normalize($object->defaultArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); - } - - try { - $v_listArray = $this->n20->normalize($object->listArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); - } - - try { - $v_iterableArray = $this->n21->normalize($object->iterableArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); - } - - try { - $v_hashMap = $this->n22->normalize($object->hashMap, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); - } - - try { - $v_nested = $this->n23->normalize($object->nested, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); - } - - try { - $v_shapeArray = $this->n24->normalize($object->shapeArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); - } - - return [ - 'defaultArray' => $v_defaultArray, - 'listArray' => $v_listArray, - 'iterableArray' => $v_iterableArray, - 'hashMap' => $v_hashMap, - 'nested' => $v_nested, - 'jsonArray' => $object->jsonArray, - 'shapeArray' => $v_shapeArray, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_defaultArray = $this->n19->normalize($object->defaultArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); - } - - try { - $v_listArray = $this->n20->normalize($object->listArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); - } - - try { - $v_iterableArray = $this->n21->normalize($object->iterableArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); - } - - try { - $v_hashMap = $this->n22->normalize($object->hashMap, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); - } - - try { - $v_nested = $this->n23->normalize($object->nested, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); - } - - try { - $v_shapeArray = $this->n24->normalize($object->shapeArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); - } - - return [ - 'defaultArray' => $v_defaultArray, - 'listArray' => $v_listArray, - 'iterableArray' => $v_iterableArray, - 'hashMap' => $v_hashMap, - 'nested' => $v_nested, - 'jsonArray' => $object->jsonArray, - 'shapeArray' => $v_shapeArray, - ]; - } - - private function recursive11(): bool - { - if ($this->recursive11 !== null) { - return $this->recursive11; - } - - $this->recursive11 = true; - - return $this->recursive11 = $this->n19 instanceof HydratorAwareNormalizer - || $this->n20 instanceof HydratorAwareNormalizer - || $this->n21 instanceof HydratorAwareNormalizer - || $this->n22 instanceof HydratorAwareNormalizer - || $this->n23 instanceof HydratorAwareNormalizer - || $this->n24 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate12(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate12($data, $context, $object); - } - - return $this->hydrate12($data, $context); - } - - public function hydrate12(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r12->newInstanceWithoutConstructor(); - - if (\array_key_exists('status', $data)) { - try { - $value = $this->n25->denormalize($data['status'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); - } - - try { - $object->status = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $e); - } - } - - if (\array_key_exists('profileCreated', $data)) { - try { - $value = $data['profileCreated']; - - if ($this->ih5 && \is_array($value)) { - $value = $this->hydrate13($value, $context); - } else { - $value = $this->n26->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); - } - - try { - $object->profileCreated = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $e); - } - } - - if (\array_key_exists('defaultArray', $data)) { - try { - $value = $this->n29->denormalize($data['defaultArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); - } - - try { - $object->defaultArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $e); - } - } else { - $object->defaultArray = []; - } - - if (\array_key_exists('listArray', $data)) { - try { - $value = $this->n30->denormalize($data['listArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); - } - - try { - $object->listArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $e); - } - } else { - $object->listArray = []; - } - - if (\array_key_exists('iterableArray', $data)) { - try { - $value = $this->n31->denormalize($data['iterableArray'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); - } - - try { - $object->iterableArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $e); - } - } else { - $object->iterableArray = []; - } - - if (\array_key_exists('skillsHashMap', $data)) { - try { - $value = $data['skillsHashMap']; - - if ($this->ih6 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->hydrate14($item, $context) : $this->n33->denormalize($item, $context); - } - - $value = $items; - } else { - $value = $this->n32->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); - } - - try { - $object->skillsHashMap = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $e); - } - } else { - $object->skillsHashMap = []; - } - - if (\array_key_exists('jsonArray', $data)) { - $value = $data['jsonArray']; - - try { - $object->jsonArray = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'jsonArray', $e); - } - } else { - $object->jsonArray = NULL; - } - - return $object; - } - - public function extract12(object $object, array $context): array - { - if ($this->tracked12) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class; - - try { - try { - $v_status = $this->n25->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); - } - - try { - $value = $object->profileCreated; - $v_profileCreated = $this->ie5 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract13($value, $context) : $this->n26->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); - } - - try { - $v_defaultArray = $this->n29->normalize($object->defaultArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); - } - - try { - $v_listArray = $this->n30->normalize($object->listArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); - } - - try { - $v_iterableArray = $this->n31->normalize($object->iterableArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); - } - - try { - $value = $object->skillsHashMap; - - if ($this->ie6 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract14($item, $context) : $this->n33->normalize($item, $context); - } - - $v_skillsHashMap = $items; - } else { - $v_skillsHashMap = $this->n32->normalize($value, $context); - } - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); - } - - return [ - 'status' => $v_status, - 'profileCreated' => $v_profileCreated, - 'defaultArray' => $v_defaultArray, - 'listArray' => $v_listArray, - 'iterableArray' => $v_iterableArray, - 'skillsHashMap' => $v_skillsHashMap, - 'jsonArray' => $object->jsonArray, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_status = $this->n25->normalize($object->status, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); - } - - try { - $value = $object->profileCreated; - $v_profileCreated = $this->ie5 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract13($value, $context) : $this->n26->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); - } - - try { - $v_defaultArray = $this->n29->normalize($object->defaultArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); - } - - try { - $v_listArray = $this->n30->normalize($object->listArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); - } - - try { - $v_iterableArray = $this->n31->normalize($object->iterableArray, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); - } - - try { - $value = $object->skillsHashMap; - - if ($this->ie6 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract14($item, $context) : $this->n33->normalize($item, $context); - } - - $v_skillsHashMap = $items; - } else { - $v_skillsHashMap = $this->n32->normalize($value, $context); - } - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); - } - - return [ - 'status' => $v_status, - 'profileCreated' => $v_profileCreated, - 'defaultArray' => $v_defaultArray, - 'listArray' => $v_listArray, - 'iterableArray' => $v_iterableArray, - 'skillsHashMap' => $v_skillsHashMap, - 'jsonArray' => $object->jsonArray, - ]; - } - - private function recursive12(): bool - { - if ($this->recursive12 !== null) { - return $this->recursive12; - } - - $this->recursive12 = true; - - return $this->recursive12 = $this->n25 instanceof HydratorAwareNormalizer - || ($this->ie5 ? $this->recursive13() : $this->n26 instanceof HydratorAwareNormalizer) - || $this->n29 instanceof HydratorAwareNormalizer - || $this->n30 instanceof HydratorAwareNormalizer - || $this->n31 instanceof HydratorAwareNormalizer - || ($this->ie6 ? false : $this->n32 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate13(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate13($data, $context, $object); - } - - return $this->hydrate13($data, $context); - } - - public function hydrate13(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r13->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n27->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n28->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $e); - } - } - - return $object; - } - - public function extract13(object $object, array $context): array - { - if ($this->tracked13) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class; - - try { - try { - $v_profileId = $this->n27->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); - } - - try { - $v_email = $this->n28->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n27->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); - } - - try { - $v_email = $this->n28->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } - - private function recursive13(): bool - { - if ($this->recursive13 !== null) { - return $this->recursive13; - } - - $this->recursive13 = true; - - return $this->recursive13 = $this->n27 instanceof HydratorAwareNormalizer - || $this->n28 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate14(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate14($data, $context, $object); - } - - return $this->hydrate14($data, $context); - } - - public function hydrate14(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r14->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\Skill::class, 'name', $e); - } - } - - return $object; - } - - public function extract14(object $object, array $context): array - { - - - return [ - 'name' => $object->name, - ]; - } - - public function fastHydrate15(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate15($data, $context, $object); - } - - return $this->hydrate15($data, $context); - } - - public function hydrate15(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r15->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n34->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n35->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $e); - } - } - - return $object; - } - - public function extract15(object $object, array $context): array - { - if ($this->tracked15) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class; - - try { - try { - $v_profileId = $this->n34->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); - } - - try { - $v_email = $this->n35->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n34->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); - } - - try { - $v_email = $this->n35->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'email' => $v_email, - ]; - } - - private function recursive15(): bool - { - if ($this->recursive15 !== null) { - return $this->recursive15; - } - - $this->recursive15 = true; - - return $this->recursive15 = $this->n34 instanceof HydratorAwareNormalizer - || $this->n35 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate16(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate16($data, $context, $object); - } - - return $this->hydrate16($data, $context); - } - - public function hydrate16(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r16->newInstanceWithoutConstructor(); - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n36->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $e); - } - } - - return $object; - } - - public function extract16(object $object, array $context): array - { - if ($this->tracked16) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class; - - try { - try { - $v_email = $this->n36->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); - } - - return [ - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_email = $this->n36->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); - } - - return [ - 'email' => $v_email, - ]; - } - - private function recursive16(): bool - { - if ($this->recursive16 !== null) { - return $this->recursive16; - } - - $this->recursive16 = true; - - return $this->recursive16 = $this->n36 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate17(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate17($data, $context, $object); - } - - return $this->hydrate17($data, $context); - } - - public function hydrate17(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r17->newInstanceWithoutConstructor(); - - if (\array_key_exists('profileId', $data)) { - try { - $value = $this->n37->denormalize($data['profileId'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $e); - } - } - - if (\array_key_exists('valueObject', $data)) { - try { - $value = $this->n38->denormalize($data['valueObject'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); - } - - try { - $object->valueObject = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $e); - } - } - - return $object; - } - - public function extract17(object $object, array $context): array - { - if ($this->tracked17) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class; - - try { - try { - $v_profileId = $this->n37->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); - } - - try { - $v_valueObject = $this->n38->normalize($object->valueObject, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'valueObject' => $v_valueObject, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n37->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); - } - - try { - $v_valueObject = $this->n38->normalize($object->valueObject, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); - } - - return [ - 'profileId' => $v_profileId, - 'valueObject' => $v_valueObject, - ]; - } - - private function recursive17(): bool - { - if ($this->recursive17 !== null) { - return $this->recursive17; - } - - $this->recursive17 = true; - - return $this->recursive17 = $this->n37 instanceof HydratorAwareNormalizer - || $this->n38 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate18(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return ($this->h18)($data, $context, $object); - } - - return ($this->h18)($data, $context); - } - - private function recursive18(): bool - { - if ($this->recursive18 !== null) { - return $this->recursive18; - } - - $this->recursive18 = true; - - return $this->recursive18 = $this->n39 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate19(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return ($this->h19)($data, $context, $object); - } - - return ($this->h19)($data, $context); - } - - private function recursive19(): bool - { - if ($this->recursive19 !== null) { - return $this->recursive19; - } - - $this->recursive19 = true; - - return $this->recursive19 = $this->n40 instanceof HydratorAwareNormalizer - || $this->n41 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate20(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate20($data, $context, $object); - } - - return $this->hydrate20($data, $context); - } - - public function hydrate20(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r20->newInstanceWithoutConstructor(); - - if (\array_key_exists('id', $data)) { - try { - $value = $this->n42->denormalize($data['id'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); - } - - try { - $object->profileId = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $e); - } - } - - if (\array_key_exists('email', $data)) { - try { - $value = $this->n43->denormalize($data['email'], $context); - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); - } - - try { - $object->email = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $e); - } - } - - return $object; - } - - public function extract20(object $object, array $context): array - { - if ($this->tracked20) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class; - - try { - try { - $v_profileId = $this->n42->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); - } - - try { - $v_email = $this->n43->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); - } - - return [ - 'id' => $v_profileId, - 'email' => $v_email, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $v_profileId = $this->n42->normalize($object->profileId, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); - } - - try { - $v_email = $this->n43->normalize($object->email, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); - } - - return [ - 'id' => $v_profileId, - 'email' => $v_email, - ]; - } - - private function recursive20(): bool - { - if ($this->recursive20 !== null) { - return $this->recursive20; - } - - $this->recursive20 = true; - - return $this->recursive20 = $this->n42 instanceof HydratorAwareNormalizer - || $this->n43 instanceof HydratorAwareNormalizer; - } - - public function fastHydrate21(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate21($data, $context, $object); - } - - return $this->hydrate21($data, $context); - } - - public function hydrate21(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r21->newInstanceWithoutConstructor(); - - if (\array_key_exists('child', $data)) { - try { - $value = $data['child']; - - if ($this->ih7 && \is_array($value)) { - $value = $this->hydrate22($value, $context); - } else { - $value = $this->n44->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); - } - - try { - $object->child = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $e); - } - } - - if (\array_key_exists('items', $data)) { - try { - $value = $data['items']; - - if ($this->ih8 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = \is_array($item) ? $this->hydrate22($item, $context) : $this->n46->denormalize($item, $context); - } - - $value = $items; - } else { - $value = $this->n45->denormalize($value, $context); - } - } catch (Throwable $e) { - throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); - } - - try { - $object->items = $value; - } catch (TypeError $e) { - throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $e); - } - } else { - $object->items = []; - } - - return $object; - } - - public function extract21(object $object, array $context): array - { - if ($this->tracked21) { - $id = \spl_object_id($object); - - if (isset($this->callStack[$id])) { - $references = \array_values($this->callStack); - $references[] = $object::class; - - throw new CircularReference($references); - } - - $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; - - try { - try { - $value = $object->child; - $v_child = $this->ie7 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($value, $context) : $this->n44->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); - } - - try { - $value = $object->items; - - if ($this->ie8 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($item, $context) : $this->n46->normalize($item, $context); - } - - $v_items = $items; - } else { - $v_items = $this->n45->normalize($value, $context); - } - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); - } - - return [ - 'child' => $v_child, - 'items' => $v_items, - ]; - } finally { - unset($this->callStack[$id]); - } - } - - try { - $value = $object->child; - $v_child = $this->ie7 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($value, $context) : $this->n44->normalize($value, $context); - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); - } - - try { - $value = $object->items; - - if ($this->ie8 && \is_array($value)) { - $items = []; - - foreach ($value as $key => $item) { - $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($item, $context) : $this->n46->normalize($item, $context); - } - - $v_items = $items; - } else { - $v_items = $this->n45->normalize($value, $context); - } - } catch (CircularReference $e) { - throw $e; - } catch (Throwable $e) { - throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); - } - - return [ - 'child' => $v_child, - 'items' => $v_items, - ]; - } - - private function recursive21(): bool - { - if ($this->recursive21 !== null) { - return $this->recursive21; - } - - $this->recursive21 = true; - - return $this->recursive21 = ($this->ie7 ? false : $this->n44 instanceof HydratorAwareNormalizer) - || ($this->ie8 ? false : $this->n45 instanceof HydratorAwareNormalizer); - } - - public function fastHydrate22(array $data, array $context): object - { - if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { - $object = $context[Hydrator::OBJECT_TO_POPULATE]; - unset($context[Hydrator::OBJECT_TO_POPULATE]); - - return $this->hydrate22($data, $context, $object); - } - - return $this->hydrate22($data, $context); - } - - public function hydrate22(array $data, array $context, object|null $object = null): object - { - $object ??= $this->r22->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\LifecycleFixture::class, 'name', $e); - } - } - - return $object; - } - - public function extract22(object $object, array $context): array - { - - - return [ - 'name' => $object->name, - ]; - } - - private function hydrator(): StackHydrator - { - return $this->hydrator ?? throw new HydratorNotSet(self::class); - } - - /** @param string $fingerprint the {@see ClassFingerprint} of the class when the code was generated */ - private function metadata(string $class, string $fingerprint): ClassMetadata - { - $metadata = $this->hydrator()->metadata($class); - - if (ClassFingerprint::of($metadata) !== $fingerprint) { - throw new OutdatedGeneratedMiddleware($class); - } - - return $metadata; - } - - private function normalizer(ClassMetadata $metadata, string $property): Normalizer - { - return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedMiddleware($metadata->className); - } - - private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter - { - return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedMiddleware($metadata->className); - } - - private function inlineObject(Normalizer $normalizer, string $class, Skip $direction): bool - { - if (!$normalizer instanceof ObjectNormalizer) { - return false; - } - - try { - if ($normalizer->className() !== $class) { - return false; - } - } catch (Throwable) { - return false; - } - - return $this->inlinable($class, $direction); - } - - private function inlineArray(Normalizer $normalizer, string $class, Skip $direction): bool - { - return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $direction); - } - - /** - * Nested objects are only inlined when this does not change the behaviour: the nested class must be handled - * by this middleware, must not be lazy and every other middleware must skip the class in this direction. - */ - private function inlinable(string $class, Skip $direction): bool - { - // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator - $root = $this->hydrator()->rootHydrator(); - - if ($root !== $this->hydrator() && !$root instanceof GeneratedHydrator) { - return false; - } - - try { - $metadata = $this->hydrator()->metadata($class); - } catch (Throwable) { - return false; - } - - if ($metadata->normalizer !== null) { - return false; - } - - if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator()->defaultLazy())) { - return false; - } - - return $this->exclusive($metadata, $direction); - } - - /** Whether every other middleware skips the class in this direction, so this middleware is the only one to run. */ - private function exclusive(ClassMetadata $metadata, Skip $direction): bool - { - foreach ($this->others as $middleware) { - if (!$middleware instanceof SkippableMiddleware) { - return false; - } - - $skip = $middleware->skip($metadata); - - if ($skip !== Skip::Both && $skip !== $direction) { - return false; - } - } - - return true; - } -} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php new file mode 100644 index 00000000..f9210f63 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php @@ -0,0 +1,255 @@ + */ + public array $middlewares; + + public CallStack $callStack; + + public Normalizer $n0; + public bool $ready0 = false; + public bool $initializing0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + + /** @throws OutdatedGeneratedTransformer */ + public function __construct(public readonly StackHydrator $hydrator) + { + $this->callStack = $hydrator->callStack(); + $this->middlewares = $hydrator->middlewares(); + + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + + $this->init0(); + } + + public function hydrate(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function extract(object $object, array $context): array + { + return $this->extract0($object, $context); + } + + private function init0(): void + { + if ($this->ready0 || $this->initializing0) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized class behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing0 = true; + + try { + $metadata = $this->hydrator->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + + $this->ready0 = true; + } finally { + $this->initializing0 = false; + } + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n0->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $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\WrongNormalizer::class; + + try { + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedTransformer($metadata->className); + } + + /** @param string $fingerprint the {@see ClassFingerprint} of the nested class when the code was generated */ + private function inlineObject(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $fingerprint, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, string $fingerprint, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $fingerprint, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the code of the nested class must be + * up to date, the class must not be lazy and every middleware must skip it in this direction. Otherwise + * they take the regular path through the hydrator. + */ + private function inlinable(string $class, string $fingerprint, Skip $direction): bool + { + // a decorator around the hydrator (e.g. tracing) has to see nested objects, they must go through the root hydrator + if ($this->hydrator->rootHydrator() !== $this->hydrator) { + return false; + } + + try { + $metadata = $this->hydrator->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null || ClassFingerprint::of($metadata) !== $fingerprint) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator->defaultLazy())) { + return false; + } + + foreach ($this->middlewares as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/TransformerCompilerTest.php b/tests/Unit/Extension/Generated/TransformerCompilerTest.php new file mode 100644 index 00000000..f9a674db --- /dev/null +++ b/tests/Unit/Extension/Generated/TransformerCompilerTest.php @@ -0,0 +1,101 @@ +metadataFactory(), $path))->compile([ + '\\' . NestedLifecycleDto::class, + NestedLifecycleDto::class, + ProfileId::class, + ]); + + self::assertSame([NestedLifecycleDto::class, LifecycleFixture::class], $compiled); + + $files = glob($path . '/*.php') ?: []; + sort($files); + self::assertCount(2, $files); + self::assertStringContainsString('/LifecycleFixtureTransformer_', $files[0]); + self::assertStringContainsString('/NestedLifecycleDtoTransformer_', $files[1]); + + // the hydrator picks them up without generating anything itself + $hydrator = $builder->buildHydrator(); + self::assertInstanceOf(StackHydrator::class, $hydrator); + + $data = ['child' => [$field => 'a'], 'items' => [[$field => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertNotInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(NestedLifecycleDto::class))); + self::assertNotInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(LifecycleFixture::class))); + } + + public function testUnknownClassIsNotGeneratable(): void + { + $path = GeneratedStackHydratorTest::cachePath() . '/' . uniqid('compiled', true); + + $this->expectException(ClassNotGeneratable::class); + + /** @phpstan-ignore argument.type */ + (new TransformerCompiler(self::builder($path, uniqid('name_'))->metadataFactory(), $path))->compile(['Unknown']); + } + + public function testCompileWithoutClasses(): void + { + $path = GeneratedStackHydratorTest::cachePath() . '/' . uniqid('compiled', true); + + self::assertSame([], (new TransformerCompiler(self::builder($path, uniqid('name_'))->metadataFactory(), $path))->compile([])); + self::assertSame([], glob($path . '/*.php')); + } + + /** A unique field name gives the classes fingerprints no transformer was loaded for yet in this process. */ + private static function builder(string $path, string $field): StackHydratorBuilder + { + return (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension($path)) + ->addMetadataEnricher(new class ($field) implements MetadataEnricher { + public function __construct(private readonly string $field) + { + } + + public function enrich(ClassMetadata $classMetadata): void + { + if ($classMetadata->className !== LifecycleFixture::class) { + return; + } + + $property = $classMetadata->properties['name']; + $property->fieldName = $this->field; + } + }); + } +} diff --git a/tests/Unit/Extension/Generated/TransformerGeneratorTest.php b/tests/Unit/Extension/Generated/TransformerGeneratorTest.php new file mode 100644 index 00000000..4eeafb59 --- /dev/null +++ b/tests/Unit/Extension/Generated/TransformerGeneratorTest.php @@ -0,0 +1,384 @@ +generate(ProfileCreated::class, 'Foo\\Bar'); + $second = $generator->generate(ProfileCreated::class, 'Foo\\Bar'); + + self::assertNotNull($first); + self::assertNotNull($second); + self::assertSame($first->code, $second->code); + self::assertSame([ProfileCreated::class], $first->classes); + self::assertStringContainsString('namespace Foo;', $first->code); + self::assertStringContainsString('final class Bar implements ClassTransformer', $first->code); + self::assertStringNotContainsString('strict_types=1', $first->code); + } + + public function testNestedClassesAreReported(): void + { + $code = (new TransformerGenerator((new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory())) + ->generate(NestedLifecycleDto::class, 'Foo\\Bar'); + + self::assertNotNull($code); + self::assertSame([NestedLifecycleDto::class, LifecycleFixture::class], $code->classes); + } + + /** + * The snapshots make every change of the generated code visible in the diff of a pull request. + * Run "make snapshot" after an intended change of the generator. + */ + #[RequiresPhp('>=8.5')] + public function testGeneratedCodeMatchesSnapshot(): void + { + $generator = new TransformerGenerator(new AttributeMetadataFactory()); + $update = getenv('UPDATE_SNAPSHOTS') === '1'; + + if ($update) { + foreach (glob(__DIR__ . '/Snapshot/*.php') ?: [] as $file) { + unlink($file); + } + + if (!is_dir(__DIR__ . '/Snapshot')) { + mkdir(__DIR__ . '/Snapshot'); + } + } + + foreach (GeneratedStackHydratorTest::CLASSES as $class) { + $name = (new ReflectionClass($class))->getShortName() . 'Transformer'; + $code = $generator->generate($class, 'Patchlevel\\Hydrator\\Tests\\Snapshot\\' . $name); + + if ($code === null) { + continue; + } + + $file = sprintf('%s/Snapshot/%s.php', __DIR__, $name); + + if ($update) { + file_put_contents($file, $code->code); + } + + self::assertStringEqualsFile($file, $code->code, 'The generated code changed, run "make snapshot" if the change is intended.'); + } + } + + public function testClassesWithClassNormalizerAreSkipped(): void + { + self::assertNull((new TransformerGenerator(new AttributeMetadataFactory()))->generate(ProfileId::class, 'Foo\\Bar')); + } + + public function testAbstractClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new TransformerGenerator(new AttributeMetadataFactory()))->generate(ChildDto::class, 'Foo\\Bar'); + } + + public function testUnknownClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + /** @phpstan-ignore argument.type */ + (new TransformerGenerator(new AttributeMetadataFactory()))->generate('Unknown', 'Foo\\Bar'); + } + + public function testDefaultsWithControlCharactersAreInlinedUnchanged(): void + { + $hydrator = $this->hydrator(MultilineDefaultDto::class); + + $object = $hydrator->hydrate(MultilineDefaultDto::class, []); + + self::assertSame("foo bar\nbaz", $object->name); + self::assertSame("first\nsecond", $object->text); + self::assertSame(["a\tb" => "c\nd \$e \"f\""], $object->lines); + } + + public function testNestedClassesAreInlined(): void + { + $hydrator = $this->hydrator(NestedLifecycleDto::class); + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], self::inlined($hydrator)); + } + + public function testChangedNestedClassIsNotInlined(): void + { + // the nested class changed since the code was generated, the outer class is still the same + $hydrator = $this->hydrator(NestedLifecycleDto::class, new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + if ($classMetadata->className !== LifecycleFixture::class) { + return; + } + + $property = $classMetadata->properties['name']; + $property->fieldName = 'title'; + } + }); + + $data = ['child' => ['title' => 'a'], 'items' => [['title' => 'b']]]; + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['ih0' => false, 'ie0' => false, 'ih1' => false, 'ie1' => false], self::inlined($hydrator)); + self::assertInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(LifecycleFixture::class))); + } + + public function testOutdatedCodeIsDetected(): void + { + $this->expectException(OutdatedGeneratedTransformer::class); + + // the normalizer of the property was removed since the code was generated + $hydrator = $this->hydrator(ProfileCreated::class, self::removeNormalizer()); + $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + } + + public function testOutdatedCodeFallsBackToReflection(): void + { + $path = GeneratedStackHydratorTest::cachePath() . '/outdated-' . self::$counter++; + $files = new TransformerFiles($path); + + // outdated code stored under the name of the current metadata, which the fingerprint normally prevents + $metadataFactory = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->addMetadataEnricher(self::removeNormalizer()) + ->metadataFactory(); + + $className = $files->className($metadataFactory->metadata(ProfileCreated::class), $metadataFactory); + $code = (new TransformerGenerator(new AttributeMetadataFactory()))->generate(ProfileCreated::class, $files->fqcn($className)); + self::assertNotNull($code); + $files->write($className, $code->code); + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedTransformerExtension($path)) + ->addMetadataEnricher(self::removeNormalizer()) + ->buildHydrator(); + self::assertInstanceOf(StackHydrator::class, $hydrator); + + $object = $hydrator->hydrate(ProfileCreated::class, ['profileId' => ProfileId::fromString('1'), 'email' => 'info@patchlevel.de']); + + self::assertSame('1', $object->profileId->toString()); + self::assertInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(ProfileCreated::class))); + } + + /** @param class-string $changed a class with the same properties as FingerprintDto but one changed detail */ + #[DataProvider('changedClasses')] + public function testChangedClassGetsAnotherName(string $changed): void + { + $factory = new AttributeMetadataFactory(); + $files = new TransformerFiles('/tmp'); + + self::assertNotSame(ClassFingerprint::of($factory->metadata(FingerprintDto::class)), ClassFingerprint::of($factory->metadata($changed))); + self::assertSame( + $files->className($factory->metadata(FingerprintDto::class), $factory), + $files->className((new AttributeMetadataFactory())->metadata(FingerprintDto::class), $factory), + ); + self::assertNotSame( + $files->className($factory->metadata(FingerprintDto::class), $factory), + $files->className($factory->metadata($changed), $factory), + ); + } + + /** @param class-string $class */ + #[DataProvider('classesWithNestedClass')] + public function testChangedNestedClassGetsAnotherName(string $class): void + { + $factory = (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(); + $changed = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->addMetadataEnricher(new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + if ($classMetadata->className !== LifecycleFixture::class) { + return; + } + + $property = $classMetadata->properties['name']; + $property->fieldName = 'title'; + } + }) + ->metadataFactory(); + $files = new TransformerFiles('/tmp'); + + self::assertSame(ClassFingerprint::of($factory->metadata($class)), ClassFingerprint::of($changed->metadata($class))); + self::assertNotSame( + $files->className($factory->metadata($class), $factory), + $files->className($changed->metadata($class), $changed), + ); + } + + /** @return iterable */ + public static function classesWithNestedClass(): iterable + { + yield 'nested object first' => [NestedLifecycleDto::class]; + yield 'nested object after a scalar' => [ScalarAndNestedDto::class]; + } + + /** @return iterable */ + public static function changedClasses(): iterable + { + yield 'changed default' => [FingerprintChangedDefaultDto::class]; + yield 'changed visibility' => [FingerprintChangedVisibilityDto::class]; + } + + private static function removeNormalizer(): MetadataEnricher + { + return new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + if ($classMetadata->className !== ProfileCreated::class) { + return; + } + + $property = $classMetadata->properties['profileId']; + $property->normalizer = null; + } + }; + } + + /** + * Generates the code for the class with the metadata of a plain hydrator and loads it into a hydrator with the + * given enrichers, which can simulate classes changing after the code was generated. + * + * @param class-string $class + */ + private function hydrator(string $class, MetadataEnricher ...$enrichers): StackHydrator + { + $fqcn = 'Patchlevel\\Hydrator\\Tests\\Generated\\Transformer' . self::$counter++; + $metadataFactory = (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(); + $code = (new TransformerGenerator($metadataFactory))->generate($class, $fqcn); + self::assertNotNull($code); + + $directory = GeneratedStackHydratorTest::cachePath(); + + if (!is_dir($directory)) { + mkdir($directory, 0777, true); + } + + $file = tempnam($directory, 'transformer'); + assert($file !== false); + file_put_contents($file, $code->code); + + require $file; + unlink($file); + + $builder = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->addTransformerFactory(new class ($class, $fqcn) implements ClassTransformerFactory { + /** @param class-string $class */ + public function __construct( + private readonly string $class, + private readonly string $fqcn, + ) { + } + + public function create(ClassMetadata $metadata, StackHydrator $hydrator): ClassTransformer|null + { + if ($metadata->className !== $this->class) { + return null; + } + + $transformer = new ($this->fqcn)($hydrator); + assert($transformer instanceof ClassTransformer); + + return $transformer; + } + }); + + foreach ($enrichers as $enricher) { + $builder->addMetadataEnricher($enricher); + } + + $hydrator = $builder->buildHydrator(); + assert($hydrator instanceof StackHydrator); + + return $hydrator; + } + + /** @return array flag name => inlined */ + private static function inlined(StackHydrator $hydrator): array + { + /** @var array $flags */ + $flags = array_filter( + get_object_vars($hydrator->transformer($hydrator->metadata(NestedLifecycleDto::class))), + static fn (string $name): bool => preg_match('/^i[he]\d+$/', $name) === 1, + ARRAY_FILTER_USE_KEY, + ); + + return $flags; + } +} diff --git a/tests/Unit/Fixture/RecordingTransformerFactory.php b/tests/Unit/Fixture/RecordingTransformerFactory.php new file mode 100644 index 00000000..f605abdf --- /dev/null +++ b/tests/Unit/Fixture/RecordingTransformerFactory.php @@ -0,0 +1,71 @@ + */ + public array $created = []; + + /** @var list */ + public array $calls = []; + + /** @param list $classes */ + public function __construct( + private readonly array $classes, + ) { + } + + public function create(ClassMetadata $metadata, StackHydrator $hydrator): ClassTransformer|null + { + if (!in_array($metadata->className, $this->classes, true)) { + return null; + } + + $this->created[] = $metadata->className; + $inner = new ReflectionTransformer($metadata, $hydrator->callStack()); + + return new class ($inner, $this) implements ClassTransformer { + public function __construct( + private readonly ClassTransformer $inner, + private readonly RecordingTransformerFactory $factory, + ) { + } + + /** + * @param array $data + * @param array $context + */ + public function hydrate(array $data, array $context): object + { + $object = $this->inner->hydrate($data, $context); + $this->factory->calls[] = 'hydrate ' . $object::class; + + return $object; + } + + /** + * @param array $context + * + * @return array + */ + public function extract(object $object, array $context): array + { + $this->factory->calls[] = 'extract ' . $object::class; + + return $this->inner->extract($object, $context); + } + }; + } +} diff --git a/tests/Unit/Fixture/ScalarAndNestedDto.php b/tests/Unit/Fixture/ScalarAndNestedDto.php new file mode 100644 index 00000000..f1fcd4a0 --- /dev/null +++ b/tests/Unit/Fixture/ScalarAndNestedDto.php @@ -0,0 +1,14 @@ +middlewares()); } + public function testAddTransformerFactoryWithPriority(): void + { + $factory1 = $this->createMock(ClassTransformerFactory::class); + $factory2 = $this->createMock(ClassTransformerFactory::class); + + $builder = new StackHydratorBuilder(); + $builder->addMiddleware($this->createMock(Middleware::class)); + $builder->addTransformerFactory($factory1, 10); + $builder->addTransformerFactory($factory2, 20); + + $reflection = new ReflectionProperty(StackHydrator::class, 'transformerFactory'); + + self::assertEquals(new ChainTransformerFactory([$factory2, $factory1]), $reflection->getValue($builder->build())); + self::assertSame([$factory2, $factory1], $builder->transformerFactories()); + self::assertNull($reflection->getValue((new StackHydratorBuilder())->build())); + } + + public function testMetadataFactoryIsTheOneOfTheHydrator(): void + { + $builder = new StackHydratorBuilder(); + $builder->addMiddleware($this->createMock(Middleware::class)); + $builder->setCache($this->createMock(CacheInterface::class)); + + $reflection = new ReflectionProperty(StackHydrator::class, 'metadataFactory'); + + self::assertInstanceOf(Psr16MetadataFactory::class, $builder->metadataFactory()); + self::assertEquals($builder->metadataFactory(), $reflection->getValue($builder->build())); + } + public function testAddMetadataEnricherWithPriority(): void { $enricher1 = $this->createMock(MetadataEnricher::class); @@ -175,15 +206,13 @@ public function testBuildHydratorAppliesDecoratorsByPriority(): void { $inner = $this->createMock(Hydrator::class); $outer = $this->createMock(Hydrator::class); - $stack = null; $innerDecorator = $this->createMock(HydratorDecorator::class); $innerDecorator ->expects($this->once()) ->method('decorate') - ->willReturnCallback(static function (Hydrator $hydrator, StackHydrator $stackHydrator) use ($inner, &$stack): Hydrator { - self::assertSame($stackHydrator, $hydrator); - $stack = $stackHydrator; + ->willReturnCallback(static function (Hydrator $hydrator) use ($inner): Hydrator { + self::assertInstanceOf(StackHydrator::class, $hydrator); return $inner; }); @@ -192,9 +221,8 @@ public function testBuildHydratorAppliesDecoratorsByPriority(): void $outerDecorator ->expects($this->once()) ->method('decorate') - ->willReturnCallback(static function (Hydrator $hydrator, StackHydrator $stackHydrator) use ($inner, $outer, &$stack): Hydrator { + ->willReturnCallback(static function (Hydrator $hydrator) use ($inner, $outer): Hydrator { self::assertSame($inner, $hydrator); - self::assertSame($stack, $stackHydrator); return $outer; }); diff --git a/tests/Unit/StackHydratorTest.php b/tests/Unit/StackHydratorTest.php index 30ad164d..b107d71d 100644 --- a/tests/Unit/StackHydratorTest.php +++ b/tests/Unit/StackHydratorTest.php @@ -15,13 +15,11 @@ use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\ClassMetadata; -use Patchlevel\Hydrator\Middleware\AllMiddlewaresSkipped; use Patchlevel\Hydrator\Middleware\Middleware; use Patchlevel\Hydrator\Middleware\Skip; use Patchlevel\Hydrator\Middleware\SkippableMiddleware; use Patchlevel\Hydrator\Middleware\Stack; use Patchlevel\Hydrator\Middleware\TransformMiddleware; -use Patchlevel\Hydrator\MissingMiddlewares; use Patchlevel\Hydrator\NormalizationFailure; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; use Patchlevel\Hydrator\StackHydrator; @@ -30,6 +28,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto; +use Patchlevel\Hydrator\Tests\Unit\Fixture\CountingMiddleware; use Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\DummyMiddleware; use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; @@ -44,11 +43,14 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; +use Patchlevel\Hydrator\Tests\Unit\Fixture\RecordingTransformerFactory; use Patchlevel\Hydrator\Tests\Unit\Fixture\Skill; use Patchlevel\Hydrator\Tests\Unit\Fixture\Status; use Patchlevel\Hydrator\Tests\Unit\Fixture\StatusWithNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\ValueObject; use Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer; +use Patchlevel\Hydrator\Transformer\ChainTransformerFactory; +use Patchlevel\Hydrator\Transformer\ReflectionTransformer; use Patchlevel\Hydrator\TypeMismatch; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -56,8 +58,12 @@ use ReflectionClass; use ReflectionProperty; +use function array_keys; + #[CoversClass(StackHydrator::class)] #[CoversClass(TransformMiddleware::class)] +#[CoversClass(ReflectionTransformer::class)] +#[CoversClass(ChainTransformerFactory::class)] final class StackHydratorTest extends TestCase { private StackHydrator $hydrator; @@ -67,15 +73,30 @@ public function setUp(): void $this->hydrator = new StackHydrator(); } - public function testMissingMiddlewares(): void + public function testWithoutMiddlewares(): void { - $this->expectException(MissingMiddlewares::class); - $this->expectExceptionMessage('Missing middlewares.'); + $hydrator = new StackHydrator(new AttributeMetadataFactory(), []); + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); - new StackHydrator( - new AttributeMetadataFactory(), - [], - ); + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertSame(['profileId' => '1', 'email' => 'info@patchlevel.de'], $hydrator->extract($event)); + self::assertSame([], $hydrator->middlewares()); + } + + public function testRegisteredTransformMiddlewareEndsTheStack(): void + { + $before = new CountingMiddleware(); + $after = new CountingMiddleware(); + + $hydrator = new StackHydrator(new AttributeMetadataFactory(), [$before, new TransformMiddleware(), $after]); + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + $hydrator->extract($event); + + self::assertSame([$before], $hydrator->middlewares()); + self::assertSame([ProfileCreated::class => 1], $before->hydrated); + self::assertSame([ProfileCreated::class => 1], $before->extracted); + self::assertSame([], $after->hydrated); + self::assertSame([], $after->extracted); } public function testExtract(): void @@ -759,18 +780,18 @@ public function testAllMiddlewaresSkipped(): void $middleware ->method('skip') ->willReturn(Skip::Both); + $middleware->expects($this->never())->method('hydrate'); + $middleware->expects($this->never())->method('extract'); $hydrator = new StackHydrator( new AttributeMetadataFactory(), [$middleware], ); - $this->expectException(AllMiddlewaresSkipped::class); - $this->expectExceptionMessage( - 'All middlewares were skipped for the class "' . ProfileCreated::class . '", at least one middleware must run.', - ); + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); - $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertSame(['profileId' => '1', 'email' => 'info@patchlevel.de'], $hydrator->extract($event)); } public function testNestedObjectsUseRootHydrator(): void @@ -812,4 +833,171 @@ public function testRootHydratorIsInjectedIntoLoadedMetadata(): void $this->hydrator->setRootHydrator($root); $this->hydrator->extract($wrapper); } + + public function testTransformerFactoriesAreAskedInOrder(): void + { + $first = new RecordingTransformerFactory([ProfileCreated::class]); + $second = new RecordingTransformerFactory([ProfileCreated::class, ParentDto::class]); + + $hydrator = new StackHydrator( + (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(), + [], + transformerFactory: new ChainTransformerFactory([$first, $second]), + ); + + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + $hydrator->extract($event); + $hydrator->hydrate(ParentDto::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + + self::assertSame([ProfileCreated::class], $first->created); + self::assertSame(['hydrate ' . ProfileCreated::class, 'extract ' . ProfileCreated::class], $first->calls); + self::assertSame([ParentDto::class], $second->created); + self::assertSame(['hydrate ' . ParentDto::class], $second->calls); + self::assertInstanceOf(ReflectionTransformer::class, $hydrator->transformer($hydrator->metadata(Skill::class))); + self::assertSame($hydrator->transformer($hydrator->metadata(Skill::class)), $hydrator->transformer($hydrator->metadata(Skill::class))); + } + + public function testTransformerIsCalledWithoutStackIfNoOtherMiddlewareRuns(): void + { + $counter = new CountingMiddleware([ProfileCreated::class => Skip::Both]); + $factory = new RecordingTransformerFactory([ProfileCreated::class]); + + $hydrator = new StackHydrator( + (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(), + [$counter, new TransformMiddleware()], + transformerFactory: $factory, + ); + + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + $event = $hydrator->hydrate(ProfileCreated::class, $data); + self::assertEquals($event, $hydrator->hydrate(ProfileCreated::class, $data)); + self::assertSame($data, $hydrator->extract($event)); + self::assertSame($data, $hydrator->extract($event)); + + self::assertSame([ProfileCreated::class], $factory->created); + self::assertCount(4, $factory->calls); + self::assertSame([], $counter->hydrated); + + $direct = (new ReflectionProperty(StackHydrator::class, 'directHydrators'))->getValue($hydrator); + self::assertIsArray($direct); + self::assertSame([ProfileCreated::class], array_keys($direct)); + + $direct = (new ReflectionProperty(StackHydrator::class, 'directExtractors'))->getValue($hydrator); + self::assertIsArray($direct); + self::assertSame([ProfileCreated::class], array_keys($direct)); + } + + public function testTransformerIsCalledThroughStackIfOtherMiddlewaresRun(): void + { + $counter = new CountingMiddleware(); + $factory = new RecordingTransformerFactory([ProfileCreated::class]); + + $hydrator = new StackHydrator( + (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(), + [$counter, new TransformMiddleware()], + transformerFactory: $factory, + ); + + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + $event = $hydrator->hydrate(ProfileCreated::class, $data); + $hydrator->hydrate(ProfileCreated::class, $data); + $hydrator->extract($event); + $hydrator->extract($event); + + self::assertSame([ProfileCreated::class => 2], $counter->hydrated); + self::assertSame([ProfileCreated::class => 2], $counter->extracted); + self::assertCount(4, $factory->calls); + } + + public function testDirectTransformerStillRequiresArrayData(): void + { + $this->hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + + $this->expectException(ArrayDataRequired::class); + + $this->hydrator->hydrate(ProfileCreated::class, 'foo'); + } + + #[RequiresPhp('>=8.4')] + public function testLazyClassesAreNeverHydratedDirectly(): void + { + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + $reflection = new ReflectionClass(LazyProfileCreated::class); + + $first = $this->hydrator->hydrate(LazyProfileCreated::class, $data); + $reflection->initializeLazyObject($first); + $second = $this->hydrator->hydrate(LazyProfileCreated::class, $data); + + self::assertTrue($reflection->isUninitializedLazyObject($second)); + } + + public function testTransformMiddlewareWithoutHydratorUsesReflection(): void + { + $middleware = new TransformMiddleware(); + $metadata = (new AttributeMetadataFactory())->metadata(Skill::class); + + $skill = $middleware->hydrate($metadata, ['name' => 'php'], [], new Stack([$middleware])); + + self::assertEquals(new Skill('php'), $skill); + self::assertSame(['name' => 'php'], $middleware->extract($metadata, $skill, [], new Stack([$middleware]))); + } + + public function testOtherMiddlewareAloneIsNotSkipped(): void + { + $middleware = new class implements Middleware { + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + { + $object = $metadata->newInstance(); + (new ReflectionProperty($object, 'name'))->setValue($object, 'from middleware'); + + return $object; + } + + /** + * @param array $context + * + * @return array + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + { + return ['name' => 'from middleware']; + } + }; + + $hydrator = new StackHydrator(middlewares: [$middleware]); + + self::assertEquals(new Skill('from middleware'), $hydrator->hydrate(Skill::class, ['name' => 'php'])); + self::assertEquals(new Skill('from middleware'), $hydrator->hydrate(Skill::class, ['name' => 'php'])); + self::assertSame(['name' => 'from middleware'], $hydrator->extract(new Skill('php'))); + self::assertSame(['name' => 'from middleware'], $hydrator->extract(new Skill('php'))); + } + + #[RequiresPhp('>=8.4')] + public function testLazyClassWithOtherMiddleware(): void + { + $counter = new CountingMiddleware(); + $hydrator = new StackHydrator( + (new StackHydratorBuilder())->useExtension(new CoreExtension())->metadataFactory(), + [$counter, new TransformMiddleware()], + ); + + $event = $hydrator->hydrate(LazyProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + self::assertSame([], $counter->hydrated); + + (new ReflectionClass(LazyProfileCreated::class))->initializeLazyObject($event); + + self::assertSame([LazyProfileCreated::class => 1], $counter->hydrated); + self::assertSame('info@patchlevel.de', $event->email->toString()); + } }