From 150c742d469ef72d4fce0f2065945c5ced113b5a Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 16 Oct 2026 16:51:08 +0200 Subject: [PATCH 1/2] Generate mappers ahead of time instead of mapping with reflection The GeneratedMapperExtension registers a mapper factory which loads mappers generated by the GeneratedMapperWarmer. Classes without generated code, or whose fingerprint changed since the warmup, are mapped with reflection. This is the generator of #219 moved onto the mapper core. Nested objects are mapped with the chain the hydrator built for the nested class, so they are also handled right away when middlewares or a lazy proxy run for that class. If the chain is a generated mapper, its nested entry point is called, which skips the checks the parent mapper already did. The resolver and Direction are not needed anymore. --- .gitignore | 1 + Makefile | 4 + docs/extensions.md | 4 +- docs/generated-code.md | 108 ++ docs/hydrator.md | 1 + docs/project.json | 3 +- phpcs.xml.dist | 2 + phpstan.neon.dist | 6 + src/Extension/Generated/ChainNestedMapper.php | 41 + src/Extension/Generated/ClassFingerprint.php | 104 ++ .../Generated/ClassNotGeneratable.php | 18 + src/Extension/Generated/ClassPlan.php | 62 + src/Extension/Generated/ClassPlanner.php | 170 +++ src/Extension/Generated/CodeEmitter.php | 406 ++++++ src/Extension/Generated/GeneratedCode.php | 19 + src/Extension/Generated/GeneratedMapper.php | 175 +++ .../Generated/GeneratedMapperExtension.php | 26 + .../Generated/GeneratedMapperFactory.php | 63 + .../Generated/GeneratedMapperNotWritable.php | 18 + .../Generated/GeneratedMapperWarmer.php | 114 ++ src/Extension/Generated/Literal.php | 86 ++ src/Extension/Generated/MapperFiles.php | 59 + src/Extension/Generated/MapperGenerator.php | 56 + src/Extension/Generated/NestedMapper.php | 27 + src/Extension/Generated/NestedPlan.php | 25 + .../Generated/OutdatedGeneratedMapper.php | 21 + src/Extension/Generated/PropertyPlan.php | 46 + src/Extension/Generated/Templates.php | 286 +++++ src/Extension/Generated/ValueKind.php | 25 + src/StackHydratorBuilder.php | 22 +- .../NoCodeGenerationAtRuntimeTest.php | 43 + ...orBench.php => GeneratedHydratorBench.php} | 48 +- ...eneratedHydratorWithCryptographyBench.php} | 53 +- .../Generated/GeneratedMapperWarmerTest.php | 148 +++ .../Generated/GeneratedStackHydratorTest.php | 1113 +++++++++++++++++ .../Generated/MapperGeneratorTest.php | 208 +++ .../Generated/Snapshot/Circle1DtoMapper.php | 217 ++++ .../Generated/Snapshot/Circle2DtoMapper.php | 217 ++++ .../Generated/Snapshot/Circle3DtoMapper.php | 217 ++++ .../Snapshot/ContextAwareDtoMapper.php | 187 +++ .../Generated/Snapshot/DefaultDtoMapper.php | 271 ++++ .../Snapshot/InferNormalizerDtoMapper.php | 412 ++++++ .../InferNormalizerWithIterablesDtoMapper.php | 574 +++++++++ .../InferNormalizerWithNullableDtoMapper.php | 467 +++++++ .../Snapshot/LazyProfileCreatedMapper.php | 254 ++++ .../Snapshot/NestedLifecycleDtoMapper.php | 366 ++++++ .../NormalizerInBaseClassDefinedDtoMapper.php | 676 ++++++++++ .../Generated/Snapshot/ParentDtoMapper.php | 254 ++++ .../Snapshot/PrivateChildDtoMapper.php | 211 ++++ .../Snapshot/PrivatePropertiesDtoMapper.php | 176 +++ .../Snapshot/ProfileCreatedMapper.php | 254 ++++ ...ofileCreatedWithInlineNormalizerMapper.php | 254 ++++ .../Snapshot/ProfileCreatedWrapperMapper.php | 217 ++++ .../SensitiveDataProfileCreatedMapper.php | 254 ++++ .../Snapshot/WrongNormalizerMapper.php | 187 +++ .../Unit/Fixture/AsymmetricVisibilityDto.php | 16 + .../Fixture/AsymmetricVisibilityParentDto.php | 13 + tests/Unit/Fixture/CountingMiddleware.php | 69 + .../Fixture/FingerprintChangedDefaultDto.php | 20 + .../FingerprintChangedVisibilityDto.php | 20 + tests/Unit/Fixture/FingerprintDto.php | 20 + tests/Unit/Fixture/MultilineDefaultDto.php | 21 + tests/Unit/Fixture/NestedLifecycleDto.php | 15 + tests/Unit/Fixture/PrivateChildDto.php | 23 + tests/Unit/Fixture/PrivateParentDto.php | 25 + tests/Unit/Fixture/PrivatePropertiesDto.php | 31 + 66 files changed, 9463 insertions(+), 86 deletions(-) create mode 100644 docs/generated-code.md create mode 100644 src/Extension/Generated/ChainNestedMapper.php create mode 100644 src/Extension/Generated/ClassFingerprint.php create mode 100644 src/Extension/Generated/ClassNotGeneratable.php create mode 100644 src/Extension/Generated/ClassPlan.php create mode 100644 src/Extension/Generated/ClassPlanner.php create mode 100644 src/Extension/Generated/CodeEmitter.php create mode 100644 src/Extension/Generated/GeneratedCode.php create mode 100644 src/Extension/Generated/GeneratedMapper.php create mode 100644 src/Extension/Generated/GeneratedMapperExtension.php create mode 100644 src/Extension/Generated/GeneratedMapperFactory.php create mode 100644 src/Extension/Generated/GeneratedMapperNotWritable.php create mode 100644 src/Extension/Generated/GeneratedMapperWarmer.php create mode 100644 src/Extension/Generated/Literal.php create mode 100644 src/Extension/Generated/MapperFiles.php create mode 100644 src/Extension/Generated/MapperGenerator.php create mode 100644 src/Extension/Generated/NestedMapper.php create mode 100644 src/Extension/Generated/NestedPlan.php create mode 100644 src/Extension/Generated/OutdatedGeneratedMapper.php create mode 100644 src/Extension/Generated/PropertyPlan.php create mode 100644 src/Extension/Generated/Templates.php create mode 100644 src/Extension/Generated/ValueKind.php create mode 100644 tests/Architecture/NoCodeGenerationAtRuntimeTest.php rename tests/Benchmark/{HydratorBench.php => GeneratedHydratorBench.php} (70%) rename tests/Benchmark/{HydratorWithCryptographyBench.php => GeneratedHydratorWithCryptographyBench.php} (72%) create mode 100644 tests/Unit/Extension/Generated/GeneratedMapperWarmerTest.php create mode 100644 tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php create mode 100644 tests/Unit/Extension/Generated/MapperGeneratorTest.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/Circle1DtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/Circle2DtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/Circle3DtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/DefaultDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/ParentDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/ProfileCreatedMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedMapper.php create mode 100644 tests/Unit/Extension/Generated/Snapshot/WrongNormalizerMapper.php create mode 100644 tests/Unit/Fixture/AsymmetricVisibilityDto.php create mode 100644 tests/Unit/Fixture/AsymmetricVisibilityParentDto.php create mode 100644 tests/Unit/Fixture/CountingMiddleware.php create mode 100644 tests/Unit/Fixture/FingerprintChangedDefaultDto.php create mode 100644 tests/Unit/Fixture/FingerprintChangedVisibilityDto.php create mode 100644 tests/Unit/Fixture/FingerprintDto.php create mode 100644 tests/Unit/Fixture/MultilineDefaultDto.php create mode 100644 tests/Unit/Fixture/NestedLifecycleDto.php create mode 100644 tests/Unit/Fixture/PrivateChildDto.php create mode 100644 tests/Unit/Fixture/PrivateParentDto.php create mode 100644 tests/Unit/Fixture/PrivatePropertiesDto.php diff --git a/.gitignore b/.gitignore index a8264bba..d2bc37a2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ infection.log infection.html .phpbench/ docs_php/ +var diff --git a/Makefile b/Makefile index faf64b56..303231f9 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,10 @@ static: phpstan cs ## run stat test: phpunit ## run tests +.PHONY: snapshot +snapshot: vendor ## regenerate the snapshots of the generated mappers + UPDATE_SNAPSHOTS=1 vendor/bin/phpunit --no-coverage --filter testGeneratedCodeMatchesSnapshot + .PHONY: benchmark benchmark: vendor ## run benchmarks vendor/bin/phpbench run tests/Benchmark --report=default diff --git a/docs/extensions.md b/docs/extensions.md index 900a906a..8bedc563 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -23,13 +23,14 @@ $hydrator = (new StackHydratorBuilder()) ``` ## Built-in extensions -The library ships with three extensions out of the box: +The library ships with four extensions out of the box: | Extension | Purpose | | --- | --- | | `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. | +| `GeneratedMapperExtension` | [Generated code](generated-code.md), use generated mapping code instead of reflection. | ## Middleware @@ -232,4 +233,5 @@ final class AuditExtension implements Extension * [How to run code before extract and after hydrate](lifecycle-hooks.md) * [How to encrypt sensitive data](cryptography.md) * [How to reshape outdated stored data](upcasting.md) +* [How to speed up hydration with generated code](generated-code.md) * [How to cache the metadata](caching.md) diff --git a/docs/generated-code.md b/docs/generated-code.md new file mode 100644 index 00000000..6d003b11 --- /dev/null +++ b/docs/generated-code.md @@ -0,0 +1,108 @@ +# Generated Code + +By default the hydrator maps arrays to objects with reflection: the +`ReflectionMapper` 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 +`GeneratedMapperExtension` uses plain PHP code generated for your classes +instead. + +The generated mappers plug in as a [mapper factory](extensions.md#mapper-factories). +The generation and the runtime are strictly separated: a warmup step generates +the code ahead of time, and the hydrator only loads it. + +## Enable the extension + +Register the `GeneratedMapperExtension` with the directory which contains +the generated code. + +```php +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperExtension; +use Patchlevel\Hydrator\StackHydratorBuilder; + +$hydrator = (new StackHydratorBuilder()) + ->useExtension(new GeneratedMapperExtension(__DIR__ . '/var/cache/hydrator')) + ->build(); +``` +For every class the hydrator looks for a generated mapper in this +directory. Classes without one are mapped with reflection as usual. + +:::success +The generated code behaves exactly like the `ReflectionMapper`: 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. +::: + +## Warm up + +The `GeneratedMapperWarmer` generates the mappers and writes them to +the directory. Run it in a cache warmer or a deployment step, with the metadata +factory of the builder, so the code matches the metadata of the hydrator, +including the guessers and enrichers of all registered extensions. + +```php +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperWarmer; +use Patchlevel\Hydrator\StackHydratorBuilder; + +$cachePath = __DIR__ . '/var/cache/hydrator'; + +$builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedMapperExtension($cachePath)); + +$warmer = new GeneratedMapperWarmer($builder->metadataFactory(), $cachePath); +$warmer->warmup([ + ProfileCreated::class, + NameChanged::class, +]); +``` +Classes referenced by these classes, such as nested value objects, get a +mapper too. The warmup throws a `ClassNotGeneratable` exception for a +class which can not be generated, for example an abstract class or a class with +a class normalizer. + +:::note +Only the warmer knows how to generate code. At runtime the extension only loads +existing files, the generator is never loaded. +::: + +## Changed classes + +The name of a generated file contains a fingerprint of everything the code +depends on: the properties with their field names, normalizers, visibility and +default values. If a class changes after the warmup, its generated mapper +is simply not found and the class is mapped with reflection until you warm +up again. + +:::warning +A missing warmup does not fail, it only makes the hydration slower. Warm up the +mappers on every deployment, like you do for other generated files. +::: + +## Nested objects + +The biggest speedup comes from nested objects: instead of going through the +normalizer and the hydrator again for every nested object, the generated code +calls the [mapper](extensions.md#mapper-factories) the hydrator built for the +nested class directly. That mapper already contains the +[middlewares](extensions.md) and the [lazy](lazy.md) proxy of the nested class, +so nothing changes in behaviour. With the [cryptography](cryptography.md) +extension for example, value objects without sensitive data are mapped by their +generated mapper right away, while classes with sensitive data still go +through the middleware, without the detour through the hydrator. + +Only classes with a class normalizer are left to the normalizer. + +:::note +If a hydrator wraps the `StackHydrator`, nested objects take the way through +the normalizer for its calls, so it sees every nested object. +::: + +## Learn more + +* [How to build the hydrator](hydrator.md) +* [How to cache the metadata](caching.md) +* [How to write your own extension](extensions.md) diff --git a/docs/hydrator.md b/docs/hydrator.md index 409fb697..4df70d54 100644 --- a/docs/hydrator.md +++ b/docs/hydrator.md @@ -180,3 +180,4 @@ value does not fit the property type. * [How normalizers convert complex types](normalizer.md) * [How to hydrate objects lazily](lazy.md) * [How to hook into the hydration process](extensions.md) +* [How to speed up hydration with generated code](generated-code.md) diff --git a/docs/project.json b/docs/project.json index 1181fb49..44c77b88 100644 --- a/docs/project.json +++ b/docs/project.json @@ -18,7 +18,8 @@ "subEntries": [ { "title": "Lifecycle Hooks", "file": "lifecycle-hooks.md" }, { "title": "Cryptography", "file": "cryptography.md" }, - { "title": "Upcasting", "file": "upcasting.md" } + { "title": "Upcasting", "file": "upcasting.md" }, + { "title": "Generated Code", "file": "generated-code.md" } ] }, { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6998ee63..75142e1f 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,6 +10,8 @@ src tests + + tests/Unit/Extension/Generated/Snapshot/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 81e9ded9..7a209d3d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,8 @@ parameters: paths: - src - tests + excludePaths: + - tests/Unit/Extension/Generated/Snapshot reportUnmatchedIgnoredErrors: false services: @@ -19,3 +21,7 @@ services: class: Patchlevel\Hydrator\Tests\Architecture\ExceptionImplementsHydratorExceptionTest tags: - phpat.test + - + class: Patchlevel\Hydrator\Tests\Architecture\NoCodeGenerationAtRuntimeTest + tags: + - phpat.test diff --git a/src/Extension/Generated/ChainNestedMapper.php b/src/Extension/Generated/ChainNestedMapper.php new file mode 100644 index 00000000..4b8dc98d --- /dev/null +++ b/src/Extension/Generated/ChainNestedMapper.php @@ -0,0 +1,41 @@ + $mapper */ + public function __construct( + private readonly Mapper $mapper, + ) { + } + + /** + * @param array $data + * @param array $context + */ + public function hydrateNested(array $data, array $context): object + { + return $this->mapper->hydrate($data, $context); + } + + /** + * @param array $context + * + * @return array + */ + public function extractNested(object $object, array $context): array + { + return $this->mapper->extract($object, $context); + } +} diff --git a/src/Extension/Generated/ClassFingerprint.php b/src/Extension/Generated/ClassFingerprint.php new file mode 100644 index 00000000..24d4def1 --- /dev/null +++ b/src/Extension/Generated/ClassFingerprint.php @@ -0,0 +1,104 @@ + $metadata + * + * @template T of object + */ + public static function of(ClassMetadata $metadata): string + { + $defaults = $metadata->promotedConstructorDefaults(); + $shape = [$metadata->reflection->isFinal()]; + + foreach ($metadata->properties as $property) { + $reflection = $property->reflection; + $default = null; + + if ($reflection->isPromoted() && isset($defaults[$property->propertyName])) { + // defaults which can not be inlined are resolved at runtime, their value does not matter here + $default = Literal::export($defaults[$property->propertyName]->getDefaultValue()) ?? 'runtime'; + } + + $declaringClass = $reflection->getDeclaringClass()->getName(); + $nested = null; + + if ($property->normalizer !== null) { + $nested = self::nested($property->normalizer); + } + + $shape[] = [ + $property->propertyName, + $property->fieldName, + $property->normalizer !== null, + $reflection->getModifiers(), + // inherited properties may need the scope of their declaring class + $declaringClass === $metadata->className ? null : $declaringClass, + $default, + $nested, + ]; + } + + return substr(hash('sha256', serialize($shape)), 0, 16); + } + + /** + * Nested objects are mapped in place if they are normalized with an object normalizer, directly or in an array. + * + * @return array{class: class-string, array: bool, final: bool}|null + */ + public static function nested(Normalizer $normalizer): array|null + { + $array = false; + + if ($normalizer instanceof ArrayNormalizer) { + $array = true; + $normalizer = $normalizer->innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + $class = $normalizer->className(); + } catch (InvalidType) { + return null; + } + + if (!class_exists($class)) { + return null; + } + + return [ + 'class' => $class, + 'array' => $array, + // the generated code checks the exact class of nested objects, which is cheaper for final classes + 'final' => (new ReflectionClass($class))->isFinal(), + ]; + } +} diff --git a/src/Extension/Generated/ClassNotGeneratable.php b/src/Extension/Generated/ClassNotGeneratable.php new file mode 100644 index 00000000..72ca8299 --- /dev/null +++ b/src/Extension/Generated/ClassNotGeneratable.php @@ -0,0 +1,18 @@ + $properties + * @param bool $scopedHydrate hydrate code must run in the scope of the class + * @param bool $scopedExtract extract code must run in the scope of the class + */ + public function __construct( + public readonly ClassMetadata $metadata, + public readonly array $properties, + public readonly bool $scopedHydrate, + public readonly bool $scopedExtract, + ) { + } + + /** @return class-string */ + public function className(): string + { + return $this->metadata->className; + } + + /** Classes without normalizers can never be part of a circular reference. */ + public function leaf(): bool + { + foreach ($this->properties as $property) { + if ($property->kind !== ValueKind::Raw) { + return false; + } + } + + return true; + } + + /** @return array slot => nested class whose objects may be mapped in place */ + public function nested(): array + { + $nested = []; + + foreach ($this->properties as $property) { + if ($property->nested === null) { + continue; + } + + $nested[$property->nested->slot] = $property->nested; + } + + return $nested; + } +} diff --git a/src/Extension/Generated/ClassPlanner.php b/src/Extension/Generated/ClassPlanner.php new file mode 100644 index 00000000..66cdd7b8 --- /dev/null +++ b/src/Extension/Generated/ClassPlanner.php @@ -0,0 +1,170 @@ + nested class => slot of its mapper */ + private array $nestedSlots = []; + + /** @param ClassMetadata $metadata */ + public function plan(ClassMetadata $metadata): ClassPlan + { + $properties = []; + + foreach ($metadata->properties as $property) { + $properties[] = $this->property($metadata, $property); + } + + [$scopedHydrate, $scopedExtract] = self::scoped($metadata, $properties); + + return new ClassPlan($metadata, $properties, $scopedHydrate, $scopedExtract); + } + + /** + * Constructor visibility does not matter, the generated code runs in the scope of the class. Anonymous classes + * have no stable name the generated code could refer to. + * + * @param ClassMetadata $metadata + */ + public static function generatable(ClassMetadata $metadata): bool + { + $reflection = $metadata->reflection; + + return !$reflection->isInternal() + && !$reflection->isAbstract() + && !$reflection->isInterface() + && !$reflection->isEnum() + && !$reflection->isAnonymous(); + } + + /** @param ClassMetadata $metadata */ + private function property(ClassMetadata $metadata, PropertyMetadata $property): PropertyPlan + { + $reflection = $property->reflection; + $declaringClass = $reflection->getDeclaringClass()->getName(); + $inherited = $declaringClass !== $metadata->className; + + $kind = ValueKind::Raw; + $slot = null; + $inner = null; + $flag = null; + $nested = null; + $default = null; + $defaultSlot = null; + + $normalizer = $property->normalizer; + + if ($normalizer !== null) { + $kind = ValueKind::Normalizer; + $slot = $this->normalizerSlots++; + $nested = ClassFingerprint::nested($normalizer); + + if ($nested !== null) { + $kind = $nested['array'] ? ValueKind::NestedArray : ValueKind::NestedObject; + $flag = $this->flagSlots++; + $this->nestedSlots[$nested['class']] ??= count($this->nestedSlots); + + if ($normalizer instanceof ArrayNormalizer) { + $inner = $this->normalizerSlots++; + } + } + } + + if ($reflection->isPromoted()) { + $parameter = $metadata->promotedConstructorDefaults()[$property->propertyName] ?? null; + + if ($parameter !== null) { + // scalar defaults are inlined, everything else is resolved via reflection at runtime + $default = Literal::export($parameter->getDefaultValue()); + + if ($default === null) { + $defaultSlot = $this->defaultSlots++; + } + } + } + + return new PropertyPlan( + name: $property->propertyName, + field: $property->fieldName, + kind: $kind, + slot: $slot, + inner: $inner, + flag: $flag, + nested: $nested === null ? null : new NestedPlan( + $nested['class'], + $this->nestedSlots[$nested['class']], + $nested['final'], + ), + default: $default, + defaultSlot: $defaultSlot, + hydrateScope: $inherited && ($reflection->isPrivate() || $reflection->isReadOnly() || self::privateSet($reflection)) ? $declaringClass : null, + extractScope: $inherited && $reflection->isPrivate() ? $declaringClass : null, + ); + } + + /** + * Code runs in the scope of the class (a bound closure) only if needed, otherwise in the methods of the mapper. + * + * @param ClassMetadata $metadata + * @param list $properties + * + * @return array{bool, bool} [hydrate needs the class scope, extract needs the class scope] + */ + private static function scoped(ClassMetadata $metadata, array $properties): array + { + $hydrate = false; + $extract = false; + + foreach ($properties as $property) { + if ($property->hydrateScope !== null || $property->extractScope !== null) { + // handled by helper closures bound to the declaring class + continue; + } + + $reflection = $metadata->properties[$property->name]->reflection; + + if (!$reflection->isPublic()) { + $hydrate = true; + $extract = true; + } elseif (self::restrictedSet($reflection)) { + $hydrate = true; + } + } + + return [$hydrate, $extract]; + } + + /** Readonly and asymmetric visibility restrict writes to the class scope. */ + private static function restrictedSet(ReflectionProperty $reflection): bool + { + return $reflection->isReadOnly() || self::privateSet($reflection) || (PHP_VERSION_ID >= 80400 && $reflection->isProtectedSet()); + } + + private static function privateSet(ReflectionProperty $reflection): bool + { + return PHP_VERSION_ID >= 80400 && $reflection->isPrivateSet(); + } +} diff --git a/src/Extension/Generated/CodeEmitter.php b/src/Extension/Generated/CodeEmitter.php new file mode 100644 index 00000000..93bbe392 --- /dev/null +++ b/src/Extension/Generated/CodeEmitter.php @@ -0,0 +1,406 @@ + property declarations of the generated class */ + private array $properties = []; + + private int $helperSlots = 0; + + public function __construct( + private readonly ClassPlan $plan, + ) { + } + + public function emit(string $namespace, string $className): string + { + $plan = $this->plan; + $initialize = []; + + foreach ($plan->properties as $property) { + if ($property->defaultSlot !== null) { + $this->properties[] = sprintf('public ReflectionParameter $d%d;', $property->defaultSlot); + $initialize[] = sprintf('$this->d%d = $this->promotedDefault(%s);', $property->defaultSlot, var_export($property->name, true)); + } + + if ($property->slot === null) { + continue; + } + + $this->properties[] = sprintf('public Normalizer $n%d;', $property->slot); + $initialize[] = sprintf('$this->n%d = $this->normalizer(%s);', $property->slot, var_export($property->name, true)); + + if ($property->nested === null || $property->flag === null) { + continue; + } + + $flag = $property->flag; + $nested = $property->nested; + + $this->properties[] = sprintf('public bool $i%d = false;', $flag); + $initialize[] = sprintf('$m%d = $this->nested($this->n%d, \\%s::class);', $flag, $property->slot, $nested->class); + $initialize[] = sprintf('$this->i%1$d = $m%1$d !== null;', $flag); + $initialize[] = sprintf('if ($m%1$d !== null) { $this->x%2$d = $m%1$d; }', $flag, $nested->slot); + + if ($property->inner === null) { + continue; + } + + $this->properties[] = sprintf('public ObjectNormalizer $n%d;', $property->inner); + $initialize[] = sprintf('if ($this->i%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $flag, $property->inner, $property->slot); + } + + foreach ($plan->nested() as $slot => $nested) { + $this->properties[] = sprintf('public NestedMapper $x%d;', $slot); + } + + [$hydrate, $hydrateHelpers] = $this->hydrateBody(); + [$extract, $extractHelpers] = $this->extractBody(); + + foreach ([...$hydrateHelpers, ...$extractHelpers] as $helper) { + $initialize[] = $helper; + } + + if ($plan->scopedHydrate) { + $this->properties[] = 'public Closure $h;'; + $initialize[] = Templates::render(Templates::CLOSURE, [ + 'slot' => 'h', + 'signature' => 'object $object, array $data, array $context, bool $inline', + 'return' => 'object', + 'body' => $hydrate, + 'class' => $plan->className(), + ]); + $hydrate = 'return ($this->h)($object, $data, $context, $inline);'; + } + + if ($plan->scopedExtract) { + $this->properties[] = 'public Closure $e;'; + $initialize[] = Templates::render(Templates::CLOSURE, [ + 'slot' => 'e', + 'signature' => 'object $object, array $context, bool $inline', + 'return' => 'array', + 'body' => $extract, + 'class' => $plan->className(), + ]); + $extract = 'return ($this->e)($object, $context, $inline);'; + } + + foreach ($plan->nested() as $slot => $nested) { + // the nested mappers are initialized last, they may lead back to this one + $initialize[] = sprintf('self::initNested($this->x%d ?? null);', $slot); + } + + $code = Templates::render(Templates::MAPPER, [ + 'namespace' => $namespace, + 'className' => $className, + 'class' => $plan->className(), + 'version' => (string)MapperFiles::VERSION, + 'properties' => implode("\n", $this->properties), + // nested objects are only mapped right away for calls of the hydrator which created the mapper + 'hydrate' => self::withInline($hydrate, $plan->nested() === [] ? 'false' : Templates::OWNER), + 'extract' => self::withInline($extract, $plan->nested() === [] ? 'false' : Templates::OWNER), + // the nested entry points are only called by a parent mapper which already did these checks + 'hydrateNested' => self::withInline($hydrate, 'true'), + 'extractNested' => self::withInline($extract, 'true'), + 'initialize' => $initialize === [] ? '// nothing to resolve' : implode("\n", $initialize), + 'recursive' => $this->recursive(), + ]); + + // empty placeholders leave blank lines behind + $code = preg_replace(["/\n{3,}/", "/{\n\n/"], ["\n\n", "{\n"], $code); + assert($code !== null); + + return $code; + } + + /** Code without nested objects does not use $inline, the code of scoped classes gets it as argument. */ + private static function withInline(string $code, string $inline): string + { + if (!str_contains($code, '$inline')) { + return $code; + } + + return sprintf("\$inline = %s;\n\n%s", $inline, $code); + } + + /** + * Circular references are only possible through normalizers which extract objects with the hydrator. A class + * whose normalizers can not reach the class itself again does not need the (expensive) call stack tracking. + */ + private function recursive(): string + { + $checks = []; + + foreach ($this->plan->properties as $property) { + if ($property->kind === ValueKind::Raw) { + continue; + } + + if ($property->nested === null) { + $checks[] = sprintf('self::mayRecurse($this->n%d)', $property->slot); + + continue; + } + + $checks[] = sprintf( + '($this->i%d ? self::reaches($this->x%d) : self::mayRecurse($this->n%d))', + $property->flag, + $property->nested->slot, + $property->slot, + ); + } + + return $checks === [] ? 'false' : implode("\n || ", $checks); + } + + /** + * Generates the hydrate code, properties which need another scope are assigned in a helper closure. + * + * @return array{string, list} body and init code of the helper closures + */ + private function hydrateBody(): array + { + $main = []; + $scoped = []; + + foreach ($this->plan->properties as $property) { + if ($property->hydrateScope !== null) { + $scoped[$property->hydrateScope][] = $this->assignment($property); + + continue; + } + + $main[] = $this->assignment($property); + } + + $helpers = []; + + foreach ($scoped as $scope => $code) { + $slot = $this->helperSlots++; + $this->properties[] = sprintf('public Closure $hp%d;', $slot); + $helpers[] = Templates::render(Templates::HYDRATE_HELPER, [ + 'slot' => (string)$slot, + 'scope' => $scope, + 'body' => implode("\n", $code), + ]); + $main[] = sprintf('($this->hp%d)($object, $data, $context, $inline);', $slot); + } + + $main[] = 'return $object;'; + + return [implode("\n\n", $main), $helpers]; + } + + private function assignment(PropertyPlan $property): string + { + $template = $property->hasDefault() ? Templates::ASSIGN_DEFAULT : Templates::ASSIGN; + + return Templates::render($template, [ + 'field' => var_export($property->field, true), + 'name' => $property->name, + 'property' => var_export($property->name, true), + 'class' => $this->plan->className(), + 'value' => $this->denormalize($property, '$value'), + 'default' => $property->default ?? sprintf('$this->d%d->getDefaultValue()', $property->defaultSlot), + ]); + } + + /** Code which denormalizes the field into the given variable. */ + private function denormalize(PropertyPlan $property, string $variable): string + { + $field = var_export($property->field, true); + + if ($property->kind === ValueKind::Raw) { + return sprintf('%s = $data[%s];', $variable, $field); + } + + $inner = match ($property->kind) { + ValueKind::Normalizer => sprintf('%s = $this->n%d->denormalize($data[%s], $context);', $variable, $property->slot, $field), + ValueKind::NestedObject => Templates::render(Templates::DENORMALIZE_OBJECT, [ + 'variable' => $variable, + 'field' => $field, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'nested' => $this->nestedCall($property, 'hydrate', $variable), + ]), + ValueKind::NestedArray => Templates::render(Templates::DENORMALIZE_ARRAY, [ + 'variable' => $variable, + 'field' => $field, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'inner' => (string)$property->inner, + 'nested' => $this->nestedCall($property, 'hydrate', '$item'), + ]), + }; + + return Templates::render(Templates::DENORMALIZE, [ + 'inner' => $inner, + 'class' => $this->plan->className(), + 'property' => var_export($property->name, true), + 'slot' => (string)$property->slot, + ]); + } + + /** + * Generates the extract code, properties which need another scope are read in a helper closure. + * + * @return array{string, list} body and init code of the helper closures + */ + private function extractBody(): array + { + $pre = []; + $scoped = []; + $fields = []; + + foreach ($this->plan->properties as $property) { + $field = var_export($property->field, true); + [$code, $expression] = $this->normalize($property); + + if ($property->extractScope !== null) { + $scoped[$property->extractScope][] = [$property->name, $code, $expression]; + $fields[] = sprintf('%s => $v_%s,', $field, $property->name); + + continue; + } + + if ($code !== '') { + $pre[] = $code; + } + + $fields[] = sprintf('%s => %s,', $field, $expression); + } + + $helpers = []; + + foreach ($scoped as $scope => $items) { + $slot = $this->helperSlots++; + $this->properties[] = sprintf('public Closure $ep%d;', $slot); + + $body = []; + $values = []; + $variables = []; + + foreach ($items as [$name, $code, $expression]) { + if ($code !== '') { + $body[] = $code; + } + + $values[] = $expression; + $variables[] = sprintf('$v_%s', $name); + } + + $body[] = sprintf('return [%s];', implode(', ', $values)); + + $helpers[] = Templates::render(Templates::EXTRACT_HELPER, [ + 'slot' => (string)$slot, + 'scope' => $scope, + 'body' => implode("\n", $body), + ]); + + array_unshift($pre, sprintf('[%s] = ($this->ep%d)($object, $context, $inline);', implode(', ', $variables), $slot)); + } + + $variables = [ + 'class' => $this->plan->className(), + 'pre' => implode("\n\n", $pre), + 'fields' => implode("\n", $fields), + ]; + + if ($this->plan->leaf()) { + return [Templates::render(Templates::EXTRACT_PLAIN, $variables), $helpers]; + } + + return [ + Templates::render(Templates::EXTRACT_TRACKED, $variables) . "\n\n" + . Templates::render(Templates::EXTRACT_PLAIN, $variables), + $helpers, + ]; + } + + /** @return array{string, string} preparing code and the expression for the normalized value */ + private function normalize(PropertyPlan $property): array + { + $name = $property->name; + + if ($property->kind === ValueKind::Raw) { + return ['', sprintf('$object->%s', $name)]; + } + + $variable = sprintf('$v_%s', $name); + + $inner = match ($property->kind) { + ValueKind::Normalizer => sprintf('%s = $this->n%d->normalize($object->%s, $context);', $variable, $property->slot, $name), + ValueKind::NestedObject => Templates::render(Templates::NORMALIZE_OBJECT, [ + 'variable' => $variable, + 'name' => $name, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'nested' => $this->nestedCall($property, 'extract', '$value'), + 'check' => self::instanceCheck('$value', $property), + ]), + ValueKind::NestedArray => Templates::render(Templates::NORMALIZE_ARRAY, [ + 'variable' => $variable, + 'name' => $name, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'inner' => (string)$property->inner, + 'nested' => $this->nestedCall($property, 'extract', '$item'), + 'check' => self::instanceCheck('$item', $property), + ]), + }; + + $code = Templates::render(Templates::NORMALIZE, [ + 'inner' => $inner, + 'class' => $this->plan->className(), + 'property' => var_export($name, true), + 'slot' => (string)$property->slot, + ]); + + return [$code, $variable]; + } + + /** Only exact instances are mapped in place, subclasses take the generic path through the hydrator. */ + private static function instanceCheck(string $variable, PropertyPlan $property): string + { + $nested = $property->nested; + assert($nested !== null); + + if ($nested->final) { + return sprintf('%s instanceof \\%s', $variable, $nested->class); + } + + return sprintf('%1$s instanceof \\%2$s && %1$s::class === \\%2$s::class', $variable, $nested->class); + } + + /** The call of the nested entry point, it skips the checks this mapper already did. */ + private function nestedCall(PropertyPlan $property, string $direction, string $value): string + { + assert($property->nested !== null); + + return sprintf('$this->x%d->%sNested(%s, $context)', $property->nested->slot, $direction, $value); + } +} diff --git a/src/Extension/Generated/GeneratedCode.php b/src/Extension/Generated/GeneratedCode.php new file mode 100644 index 00000000..fef1caa4 --- /dev/null +++ b/src/Extension/Generated/GeneratedCode.php @@ -0,0 +1,19 @@ + $nested classes whose objects may be mapped in place, they need a mapper too + */ + public function __construct( + public readonly string $code, + public readonly array $nested, + ) { + } +} diff --git a/src/Extension/Generated/GeneratedMapper.php b/src/Extension/Generated/GeneratedMapper.php new file mode 100644 index 00000000..6af6e333 --- /dev/null +++ b/src/Extension/Generated/GeneratedMapper.php @@ -0,0 +1,175 @@ + + */ +abstract class GeneratedMapper implements Mapper, NestedMapper +{ + public bool $ready = false; + public bool $initializing = false; + public bool|null $recursive = null; + + /** Whether extracted objects are tracked to detect circular references. */ + public bool $tracked = false; + + /** @var ReflectionClass */ + public ReflectionClass $reflection; + + /** @param ClassMetadata $metadata */ + public function __construct( + public readonly ClassMetadata $metadata, + public readonly StackHydrator $hydrator, + public readonly CallStack $callStack, + ) { + } + + final public function init(): void + { + if ($this->ready || $this->initializing) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized mapper behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing = true; + + try { + $this->reflection = $this->metadata->reflection; + $this->initialize(); + $this->tracked = $this->recursive(); + $this->ready = true; + } finally { + $this->initializing = false; + } + } + + /** Whether an extracted object can lead back to an object of this class, see {@see self::$tracked}. */ + final public function recursive(): bool + { + if ($this->recursive !== null) { + return $this->recursive; + } + + // cycles are treated as recursive + $this->recursive = true; + + return $this->recursive = $this->mayReachItself(); + } + + /** Resolves the normalizers and the mappers of the nested classes. */ + abstract protected function initialize(): void; + + abstract protected function mayReachItself(): bool; + + final protected function normalizer(string $property): Normalizer + { + return $this->metadata->properties[$property]->normalizer + ?? throw new OutdatedGeneratedMapper($this->metadata->className); + } + + final protected function promotedDefault(string $property): ReflectionParameter + { + return $this->metadata->promotedConstructorDefaults()[$property] + ?? throw new OutdatedGeneratedMapper($this->metadata->className); + } + + /** + * The mapper the hydrator runs for the nested class, if the objects of the property can be mapped with it right + * away: the property still has the object normalizer of the class the code was generated for and the nested + * class has no class normalizer. A generated mapper is called directly, any other chain, for example with + * middlewares, through a {@see ChainNestedMapper}. + * + * @param class-string $class + */ + final protected function nested(Normalizer $normalizer, string $class): NestedMapper|null + { + if ($normalizer instanceof ArrayNormalizer) { + $normalizer = $normalizer->innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + if ($normalizer->className() !== $class) { + return null; + } + + $mapper = $this->hydrator->mapper($class); + } catch (Throwable) { + return null; + } + + if ($mapper === null || $mapper instanceof self) { + return $mapper; + } + + return new ChainNestedMapper($mapper); + } + + /** Initializes a nested generated mapper, the nested class may lead back to this one. */ + final protected static function initNested(NestedMapper|null $mapper): void + { + if (!$mapper instanceof self) { + return; + } + + $mapper->init(); + } + + /** Whether objects extracted with the mapper can lead back to the same object. */ + final protected static function reaches(NestedMapper $mapper): bool + { + // the middlewares and the lazy proxy of a chain are not known, they might + return !$mapper instanceof self || $mapper->recursive(); + } + + /** Whether the normalizer extracts objects with the hydrator, which can lead back to the same object. */ + final protected static function mayRecurse(Normalizer $normalizer): bool + { + if ($normalizer instanceof ArrayNormalizer) { + return self::mayRecurse($normalizer->innerNormalizer()); + } + + if ($normalizer instanceof ArrayShapeNormalizer) { + foreach ($normalizer->innerNormalizers() as $inner) { + if (self::mayRecurse($inner)) { + return true; + } + } + + return false; + } + + return $normalizer instanceof ObjectNormalizer || $normalizer instanceof ObjectMapNormalizer; + } +} diff --git a/src/Extension/Generated/GeneratedMapperExtension.php b/src/Extension/Generated/GeneratedMapperExtension.php new file mode 100644 index 00000000..05f3cb32 --- /dev/null +++ b/src/Extension/Generated/GeneratedMapperExtension.php @@ -0,0 +1,26 @@ +addMapperFactory(new GeneratedMapperFactory(new MapperFiles($this->cachePath))); + } +} diff --git a/src/Extension/Generated/GeneratedMapperFactory.php b/src/Extension/Generated/GeneratedMapperFactory.php new file mode 100644 index 00000000..f41da6a1 --- /dev/null +++ b/src/Extension/Generated/GeneratedMapperFactory.php @@ -0,0 +1,63 @@ + the generated mappers of a hydrator share their call stack */ + private WeakMap $callStacks; + + public function __construct( + private readonly MapperFiles $files, + ) { + $this->callStacks = new WeakMap(); + } + + /** + * @param ClassMetadata $metadata + * + * @return Mapper|null + * + * @template T of object + */ + public function create(ClassMetadata $metadata, StackHydrator $hydrator): Mapper|null + { + $className = $this->files->className($metadata); + $fqcn = $this->files->fqcn($className); + + if (!class_exists($fqcn, false)) { + $file = $this->files->path($className); + + if (!is_file($file)) { + return null; + } + + require_once $file; + } + + /** @var Mapper $mapper */ + $mapper = new $fqcn($metadata, $hydrator, $this->callStacks[$hydrator] ??= new CallStack()); + assert($mapper instanceof GeneratedMapper); + + return $mapper; + } +} diff --git a/src/Extension/Generated/GeneratedMapperNotWritable.php b/src/Extension/Generated/GeneratedMapperNotWritable.php new file mode 100644 index 00000000..c9a85ba9 --- /dev/null +++ b/src/Extension/Generated/GeneratedMapperNotWritable.php @@ -0,0 +1,18 @@ +metadataFactory(), $cachePath))->warmup([ProfileCreated::class]); + */ +final class GeneratedMapperWarmer +{ + private readonly MapperFiles $files; + + public function __construct( + private readonly MetadataFactory $metadataFactory, + string $cachePath, + ) { + $this->files = new MapperFiles($cachePath); + } + + /** + * Generates a mapper for every class and every nested class whose objects may be mapped in place. + * + * @param list $classes + * + * @return list all classes a mapper was generated for + * + * @throws ClassNotGeneratable if a class of the list can not be generated. + * @throws GeneratedMapperNotWritable + */ + public function warmup(array $classes): array + { + $generator = new MapperGenerator(); + $queue = []; + + foreach ($classes as $class) { + $queue[] = [ltrim($class, '\\'), true]; + } + + $done = []; + $generated = []; + + while ($queue !== []) { + [$class, $requested] = array_shift($queue); + + if (isset($done[$class])) { + continue; + } + + $done[$class] = true; + + try { + $metadata = $this->metadataFactory->metadata($class); + $className = $this->files->className($metadata); + $code = $generator->generate($metadata, $this->files->fqcn($className)); + } catch (ClassNotFound) { + if ($requested) { + throw new ClassNotGeneratable($class, 'class not found'); + } + + continue; + } catch (ClassNotGeneratable $e) { + // nested classes without a mapper are hydrated by the hydrator + if ($requested) { + throw $e; + } + + continue; + } + + $this->write($this->files->path($className), $code->code); + $generated[] = $metadata->className; + + foreach ($code->nested as $nested) { + $queue[] = [$nested, false]; + } + } + + return $generated; + } + + /** @throws GeneratedMapperNotWritable */ + private function write(string $file, string $code): void + { + $directory = $this->files->cachePath; + + if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new GeneratedMapperNotWritable($file); + } + + $tmp = $file . '.' . uniqid('', true) . '.tmp'; + + if (@file_put_contents($tmp, $code) === false || !@rename($tmp, $file)) { + @unlink($tmp); + + throw new GeneratedMapperNotWritable($file); + } + } +} diff --git a/src/Extension/Generated/Literal.php b/src/Extension/Generated/Literal.php new file mode 100644 index 00000000..3e469bbe --- /dev/null +++ b/src/Extension/Generated/Literal.php @@ -0,0 +1,86 @@ +name); + } + + if (is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $literal = self::export($item); + + if ($literal === null) { + return null; + } + + $items[] = sprintf('%s => %s', is_string($key) ? self::string($key) : var_export($key, true), $literal); + } + + return '[' . implode(', ', $items) . ']'; + } + + return null; + } + + /** + * var_export() writes control characters like newlines verbatim, which breaks the indentation of the + * generated code. Such strings are written as double quoted literals with escape sequences instead. + */ + private static function string(string $value): string + { + if (preg_match('/[\x00-\x1f\x7f]/', $value) !== 1) { + return var_export($value, true); + } + + $escaped = preg_replace_callback( + '/[\x00-\x1f\x7f"\\\\$]/', + static fn (array $matches): string => match ($matches[0]) { + '"' => '\\"', + '\\' => '\\\\', + '$' => '\\$', + default => sprintf('\\x%02X', ord($matches[0])), + }, + $value, + ); + + return '"' . $escaped . '"'; + } +} diff --git a/src/Extension/Generated/MapperFiles.php b/src/Extension/Generated/MapperFiles.php new file mode 100644 index 00000000..3fb90349 --- /dev/null +++ b/src/Extension/Generated/MapperFiles.php @@ -0,0 +1,59 @@ + $metadata + * + * @template T of object + */ + public function className(ClassMetadata $metadata): string + { + $hash = hash('sha256', serialize([self::VERSION, $metadata->className, ClassFingerprint::of($metadata)])); + + return sprintf( + '%sMapper_%s', + preg_replace('/[^A-Za-z0-9_]/', '', $metadata->reflection->getShortName()), + substr($hash, 0, 16), + ); + } + + public function fqcn(string $className): string + { + return self::NAMESPACE . '\\' . $className; + } + + public function path(string $className): string + { + return sprintf('%s/%s.php', $this->cachePath, $className); + } +} diff --git a/src/Extension/Generated/MapperGenerator.php b/src/Extension/Generated/MapperGenerator.php new file mode 100644 index 00000000..230f1d79 --- /dev/null +++ b/src/Extension/Generated/MapperGenerator.php @@ -0,0 +1,56 @@ + $metadata + * + * @throws ClassNotGeneratable + * + * @template T of object + */ + public function generate(ClassMetadata $metadata, string $fqcn): GeneratedCode + { + if (!ClassPlanner::generatable($metadata)) { + throw new ClassNotGeneratable($metadata->className, 'the class is internal, abstract, anonymous, an enum or an interface'); + } + + if ($metadata->normalizer !== null) { + throw new ClassNotGeneratable($metadata->className, 'the class has a class normalizer'); + } + + $plan = (new ClassPlanner())->plan($metadata); + + $parts = explode('\\', ltrim($fqcn, '\\')); + $className = array_pop($parts); + $namespace = implode('\\', $parts); + + $nested = []; + + foreach ($plan->nested() as $class) { + $nested[] = $class->class; + } + + return new GeneratedCode( + (new CodeEmitter($plan))->emit($namespace, $className), + $nested, + ); + } +} diff --git a/src/Extension/Generated/NestedMapper.php b/src/Extension/Generated/NestedMapper.php new file mode 100644 index 00000000..e59a0509 --- /dev/null +++ b/src/Extension/Generated/NestedMapper.php @@ -0,0 +1,27 @@ + $data + * @param array $context the call comes from the hydrator of the mapper, without an object to populate + */ + public function hydrateNested(array $data, array $context): object; + + /** + * @param array $context the call comes from the hydrator of the mapper + * + * @return array + */ + public function extractNested(object $object, array $context): array; +} diff --git a/src/Extension/Generated/NestedPlan.php b/src/Extension/Generated/NestedPlan.php new file mode 100644 index 00000000..e9b78ae8 --- /dev/null +++ b/src/Extension/Generated/NestedPlan.php @@ -0,0 +1,25 @@ +default !== null || $this->defaultSlot !== null; + } +} diff --git a/src/Extension/Generated/Templates.php b/src/Extension/Generated/Templates.php new file mode 100644 index 00000000..935ce85e --- /dev/null +++ b/src/Extension/Generated/Templates.php @@ -0,0 +1,286 @@ + $variables */ + public static function render(string $template, array $variables): string + { + $template = trim($template, "\n"); + + foreach ($variables as $name => $value) { + $template = self::replaceIndented($template, $name, $value); + } + + return $template; + } + + private static function replaceIndented(string $template, string $name, string $value): string + { + $placeholder = '{{' . $name . '}}'; + + $result = preg_replace_callback( + '/^([ \t]*)' . preg_quote($placeholder, '/') . '$/m', + static function (array $matches) use ($value): string { + $lines = explode("\n", $value); + + return implode("\n", array_map( + static fn (string $line): string => $line === '' ? '' : $matches[1] . $line, + $lines, + )); + }, + $template, + ); + + return str_replace($placeholder, $value, $result ?? $template); + } + + public const MAPPER = <<<'PHP' +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + {{hydrate}} + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + {{extract}} + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + {{hydrateNested}} + } + + public function extractNested(object $object, array $context): array + { + {{extractNested}} + } + + protected function initialize(): void + { + {{initialize}} + } + + protected function mayReachItself(): bool + { + return {{recursive}}; + } +} + +PHP; + + public const CLOSURE = <<<'PHP' +$this->{{slot}} = Closure::bind(function ({{signature}}): {{return}} { + {{body}} +}, $this, \{{class}}::class); +PHP; + + public const HYDRATE_HELPER = <<<'PHP' +$this->hp{{slot}} = Closure::bind(function (object $object, array $data, array $context, bool $inline): void { + {{body}} +}, $this, \{{scope}}::class); +PHP; + + public const EXTRACT_TRACKED = <<<'PHP' +if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \{{class}}::class; + + try { + {{pre}} + + return [ + {{fields}} + ]; + } finally { + unset($this->callStack->objects[$id]); + } +} +PHP; + + /** Nested objects are only mapped right away for calls from the hydrator which created the mapper. */ + public const OWNER = '($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator'; + + public const EXTRACT_PLAIN = <<<'PHP' +{{pre}} + +return [ + {{fields}} +]; +PHP; + + public const EXTRACT_HELPER = <<<'PHP' +$this->ep{{slot}} = Closure::bind(function (object $object, array $context, bool $inline): array { + {{body}} +}, $this, \{{scope}}::class); +PHP; + + public const ASSIGN = <<<'PHP' +if (\array_key_exists({{field}}, $data)) { + {{value}} + + try { + $object->{{name}} = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\{{class}}::class, {{property}}, $e); + } +} +PHP; + + public const ASSIGN_DEFAULT = <<<'PHP' +if (\array_key_exists({{field}}, $data)) { + {{value}} + + try { + $object->{{name}} = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\{{class}}::class, {{property}}, $e); + } +} else { + $object->{{name}} = {{default}}; +} +PHP; + + public const DENORMALIZE = <<<'PHP' +try { + {{inner}} +} catch (Throwable $e) { + throw new DenormalizationFailure(\{{class}}::class, {{property}}, $this->n{{slot}}::class, $e); +} +PHP; + + public const DENORMALIZE_OBJECT = <<<'PHP' +{{variable}} = $data[{{field}}]; + +if ($this->i{{flag}} && $inline && \is_array({{variable}})) { + {{variable}} = {{nested}}; +} else { + {{variable}} = $this->n{{slot}}->denormalize({{variable}}, $context); +} +PHP; + + public const DENORMALIZE_ARRAY = <<<'PHP' +{{variable}} = $data[{{field}}]; + +if ($this->i{{flag}} && $inline && \is_array({{variable}})) { + $items = []; + + foreach ({{variable}} as $key => $item) { + $items[$key] = \is_array($item) ? {{nested}} : $this->n{{inner}}->denormalize($item, $context); + } + + {{variable}} = $items; +} else { + {{variable}} = $this->n{{slot}}->denormalize({{variable}}, $context); +} +PHP; + + public const NORMALIZE = <<<'PHP' +try { + {{inner}} +} catch (CircularReference $e) { + throw $e; +} catch (Throwable $e) { + throw new NormalizationFailure(\{{class}}::class, {{property}}, $this->n{{slot}}::class, $e); +} +PHP; + + public const NORMALIZE_OBJECT = <<<'PHP' +$value = $object->{{name}}; +{{variable}} = $this->i{{flag}} && $inline && {{check}} ? {{nested}} : $this->n{{slot}}->normalize($value, $context); +PHP; + + public const NORMALIZE_ARRAY = <<<'PHP' +$value = $object->{{name}}; + +if ($this->i{{flag}} && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = {{check}} ? {{nested}} : $this->n{{inner}}->normalize($item, $context); + } + + {{variable}} = $items; +} else { + {{variable}} = $this->n{{slot}}->normalize($value, $context); +} +PHP; +} diff --git a/src/Extension/Generated/ValueKind.php b/src/Extension/Generated/ValueKind.php new file mode 100644 index 00000000..86ea63a5 --- /dev/null +++ b/src/Extension/Generated/ValueKind.php @@ -0,0 +1,25 @@ +metadataFactory(), + $this->middlewares(), + $this->defaultLazy, + $this->mapperFactories(), + ); + } + + /** + * The metadata factory the hydrator is built with, for example to generate code or warm up caches with the same + * metadata the hydrator ends up with. Every call creates a new instance. + */ + public function metadataFactory(): MetadataFactory { $metadataFactory = new EnrichingMetadataFactory( new AttributeMetadataFactory( @@ -117,12 +132,7 @@ public function build(): StackHydrator $metadataFactory = new Psr16MetadataFactory($metadataFactory, $this->cache); } - return new StackHydrator( - $metadataFactory, - $this->middlewares(), - $this->defaultLazy, - $this->mapperFactories(), - ); + return $metadataFactory; } public function defaultLazy(): bool diff --git a/tests/Architecture/NoCodeGenerationAtRuntimeTest.php b/tests/Architecture/NoCodeGenerationAtRuntimeTest.php new file mode 100644 index 00000000..626fce65 --- /dev/null +++ b/tests/Architecture/NoCodeGenerationAtRuntimeTest.php @@ -0,0 +1,43 @@ +classes( + Selector::classname(GeneratedMapperExtension::class), + Selector::classname(GeneratedMapperFactory::class), + Selector::classname(GeneratedMapper::class), + Selector::classname(ChainNestedMapper::class), + ) + ->shouldNotDependOn() + ->classes( + Selector::classname(GeneratedMapperWarmer::class), + Selector::classname(MapperGenerator::class), + Selector::classname(CodeEmitter::class), + Selector::classname(ClassPlanner::class), + Selector::classname(ClassPlan::class), + Selector::classname(Templates::class), + ); + } +} diff --git a/tests/Benchmark/HydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php similarity index 70% rename from tests/Benchmark/HydratorBench.php rename to tests/Benchmark/GeneratedHydratorBench.php index d760e689..17da5b5d 100644 --- a/tests/Benchmark/HydratorBench.php +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -4,6 +4,8 @@ namespace Patchlevel\Hydrator\Tests\Benchmark; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperWarmer; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -12,14 +14,18 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorBench +final class GeneratedHydratorBench { private Hydrator $hydrator; public function __construct() { - $this->hydrator = (new StackHydratorBuilder()) - ->build(); + $builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedMapperExtension(__DIR__ . '/../../var/cache')); + + (new GeneratedMapperWarmer($builder->metadataFactory(), __DIR__ . '/../../var/cache'))->warmup([ProfileCreated::class]); + + $this->hydrator = $builder->build(); } public function setUp(): void @@ -39,7 +45,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -52,7 +58,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -98,36 +104,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[Bench\Revs(3)] - public function benchHydrate1000000Objects(): void - { - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->hydrate(ProfileCreated::class, [ - 'profileId' => '1', - 'name' => 'foo', - 'skills' => [ - ['name' => 'php'], - ['name' => 'symfony'], - ], - ]); - } - } - - #[Bench\Revs(3)] - public function benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Benchmark/HydratorWithCryptographyBench.php b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php similarity index 72% rename from tests/Benchmark/HydratorWithCryptographyBench.php rename to tests/Benchmark/GeneratedHydratorWithCryptographyBench.php index 0851d555..7febf333 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php @@ -7,6 +7,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\GeneratedMapperExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMapperWarmer; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -15,7 +17,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorWithCryptographyBench +final class GeneratedHydratorWithCryptographyBench { private InMemoryCipherKeyStore $store; @@ -25,9 +27,13 @@ public function __construct() { $this->store = new InMemoryCipherKeyStore(); - $this->hydrator = (new StackHydratorBuilder()) - ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))) - ->build(); + $builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedMapperExtension(__DIR__ . '/../../var/cache')) + ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))); + + (new GeneratedMapperWarmer($builder->metadataFactory(), __DIR__ . '/../../var/cache'))->warmup([ProfileCreated::class]); + + $this->hydrator = $builder->build(); } public function setUp(): void @@ -49,7 +55,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -65,7 +71,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -114,39 +120,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[Bench\Revs(3)] - public function benchHydrate1000000Objects(): void - { - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->hydrate( - ProfileCreated::class, - [ - 'profileId' => '1', - 'name' => 'foo', - 'skills' => [ - ['name' => 'php'], - ['name' => 'symfony'], - ], - ], - ); - } - } - - #[Bench\Revs(3)] - public function benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Unit/Extension/Generated/GeneratedMapperWarmerTest.php b/tests/Unit/Extension/Generated/GeneratedMapperWarmerTest.php new file mode 100644 index 00000000..c32c9664 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedMapperWarmerTest.php @@ -0,0 +1,148 @@ +cachePath = sys_get_temp_dir() . '/' . uniqid('patchlevel-hydrator-warmer-', true); + } + + public function tearDown(): void + { + if (!is_dir($this->cachePath)) { + return; + } + + foreach (glob($this->cachePath . '/*') ?: [] as $file) { + unlink($file); + } + + rmdir($this->cachePath); + } + + public function testWarmupGeneratesNestedClasses(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $warmer = new GeneratedMapperWarmer($metadataFactory, $this->cachePath); + + self::assertSame( + [ProfileCreatedWrapper::class, ProfileCreated::class], + $warmer->warmup([ProfileCreatedWrapper::class]), + ); + + $files = new MapperFiles($this->cachePath); + + foreach ([ProfileCreatedWrapper::class, ProfileCreated::class] as $class) { + self::assertFileExists($files->path($files->className($metadataFactory->metadata($class)))); + } + } + + public function testWarmupIsDeterministic(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $files = new MapperFiles($this->cachePath); + $file = $files->path($files->className($metadataFactory->metadata(ProfileCreated::class))); + + (new GeneratedMapperWarmer($metadataFactory, $this->cachePath))->warmup([ProfileCreated::class]); + $first = file_get_contents($file); + + (new GeneratedMapperWarmer($metadataFactory, $this->cachePath))->warmup([ProfileCreated::class]); + + self::assertSame($first, file_get_contents($file)); + } + + public function testWarmedMappersAreUsed(): void + { + $builder = (new StackHydratorBuilder())->useExtension(new GeneratedMapperExtension($this->cachePath)); + (new GeneratedMapperWarmer($builder->metadataFactory(), $this->cachePath))->warmup([ProfileCreatedWrapper::class]); + $hydrator = $builder->build(); + + $data = ['event' => ['profileId' => '1', 'email' => 'info@patchlevel.de']]; + $object = $hydrator->hydrate(ProfileCreatedWrapper::class, $data); + + self::assertEquals( + new ProfileCreatedWrapper(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de'))), + $object, + ); + self::assertSame($data, $hydrator->extract($object)); + + $mappers = (new ReflectionProperty(StackHydrator::class, 'mappers'))->getValue($hydrator); + self::assertIsArray($mappers); + self::assertInstanceOf(GeneratedMapper::class, $mappers[ProfileCreatedWrapper::class]); + self::assertInstanceOf(GeneratedMapper::class, $mappers[ProfileCreated::class]); + } + + public function testUnknownClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + /** @phpstan-ignore argument.type */ + (new GeneratedMapperWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup(['Unknown']); + } + + public function testAbstractClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new GeneratedMapperWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup([ChildDto::class]); + } + + public function testClassWithClassNormalizerIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new GeneratedMapperWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup([ProfileId::class]); + } + + public function testNotWritable(): void + { + $this->expectException(GeneratedMapperNotWritable::class); + + $file = $this->cachePath . '-file'; + touch($file); + + try { + (new GeneratedMapperWarmer(new AttributeMetadataFactory(), $file))->warmup([ProfileCreated::class]); + } finally { + unlink($file); + } + } +} diff --git a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php new file mode 100644 index 00000000..41a14954 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php @@ -0,0 +1,1113 @@ += 80400) { + $classes[] = AsymmetricVisibilityDto::class; + } + + (new GeneratedMapperWarmer((new StackHydratorBuilder())->metadataFactory(), self::$cachePath))->warmup($classes); + } + + public static function tearDownAfterClass(): void + { + foreach (glob(self::$cachePath . '/*.php') ?: [] as $file) { + unlink($file); + } + + rmdir(self::$cachePath); + } + + public function setUp(): void + { + $this->hydrator = $this->builder()->build(); + } + + /** + * Closures in attributes (used by the inline normalizer fixture) are only supported since PHP 8.5. + * + * @return list + */ + private static function supportedClasses(): array + { + if (PHP_VERSION_ID >= 80500) { + return self::CLASSES; + } + + return array_values(array_filter( + self::CLASSES, + static fn (string $class): bool => $class !== ProfileCreatedWithInlineNormalizer::class, + )); + } + + private function builder(): StackHydratorBuilder + { + return (new StackHydratorBuilder())->useExtension($this->extension()); + } + + private function extension(): GeneratedMapperExtension + { + return new GeneratedMapperExtension(self::$cachePath); + } + + public function testExtract(): void + { + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals( + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithInheritance(): void + { + $event = new ParentDto( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals( + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithHydratorAwareNormalizer(): void + { + $event = new ProfileCreatedWrapper( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + ); + + self::assertEquals( + ['event' => ['profileId' => '1', 'email' => 'info@patchlevel.de']], + $this->hydrator->extract($event), + ); + } + + public function testExtractPassesContextToNormalizer(): void + { + $dto = new ContextAwareDto('value'); + + $data = $this->hydrator->extract($dto, ['prefix' => 'ctx-']); + + self::assertSame(['value' => 'ctx-value'], $data); + } + + public function testExtractCircularReference(): void + { + $this->expectException(CircularReference::class); + $this->expectExceptionMessage('Circular reference detected: Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto'); + + $dto1 = new Circle1Dto(); + $dto2 = new Circle2Dto(); + $dto3 = new Circle3Dto(); + + $dto1->to = $dto2; + $dto2->to = $dto3; + $dto3->to = $dto1; + + $this->hydrator->extract($dto1); + } + + public function testExtractWithInferNormalizer(): void + { + $result = $this->hydrator->extract( + new InferNormalizerWithNullableDto( + null, + null, + profileId: ProfileId::fromString('1'), + ), + ); + + self::assertEquals( + [ + 'status' => null, + 'dateTimeImmutable' => null, + 'dateTime' => null, + 'dateTimeZone' => null, + 'profileId' => '1', + ], + $result, + ); + } + + public function testExtractWithContext(): void + { + $object = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $expect = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('extract') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $object, + $this->callback(static fn (array $context): bool => $context['context'] === '123' + && $context[Hydrator::HYDRATOR] instanceof StackHydrator), + $this->isInstanceOf(Mapper::class), + )->willReturn($expect); + + $hydrator = $this->builder() + ->addMiddleware($middleware) + ->build(); + + $data = $hydrator->extract($object, ['context' => '123']); + + self::assertEquals($expect, $data); + } + + #[RequiresPhp('>=8.5')] + public function testExtractWithInlineNormalizer(): void + { + $event = new ProfileCreatedWithInlineNormalizer( + ProfileId::fromString('1'), + ValueObject::fromString('foo'), + ); + + self::assertEquals( + ['profileId' => '1', 'valueObject' => 'foo'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithClassNormalizer(): void + { + $data = $this->hydrator->extract( + ProfileId::fromString('id'), + ); + + self::assertEquals('id', $data); + } + + public function testExtractPrivateProperties(): void + { + $dto = new PrivatePropertiesDto(ProfileId::fromString('1'), 'foo', 12); + + self::assertSame( + ['profileId' => '1', 'name' => 'foo', 'age' => 12], + $this->hydrator->extract($dto), + ); + } + + public function testExtractPrivatePropertiesOfParent(): void + { + $dto = new PrivateChildDto(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de'), 'note', 'foo'); + + self::assertSame( + ['profileId' => '1', 'name' => 'foo', 'email' => 'info@patchlevel.de', 'note' => 'note'], + $this->hydrator->extract($dto), + ); + self::assertSame((new StackHydrator())->extract($dto), $this->hydrator->extract($dto)); + } + + public function testHydrate(): void + { + $expected = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $event = $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + self::assertEquals($expected, $event); + } + + public function testHydratePassesContextToNormalizer(): void + { + $event = $this->hydrator->hydrate( + ContextAwareDto::class, + ['value' => 'value'], + ['suffix' => '-ctx'], + ); + + self::assertSame('value-ctx', $event->value); + } + + public function testHydrateUnknownClass(): void + { + $this->expectException(ClassNotSupported::class); + $this->expectExceptionCode(0); + + $this->hydrator->hydrate( + // @phpstan-ignore argument.type + 'Unknown', + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + } + + public function testHydrateWithArrayDataRequired(): void + { + $this->expectException(ArrayDataRequired::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + 'foo', + ); + } + + public function testHydrateWithDefaults(): void + { + $object = $this->hydrator->hydrate( + DefaultDto::class, + ['name' => 'test'], + ); + + self::assertEquals('test', $object->name); + self::assertEquals(new Email('info@patchlevel.de'), $object->email); + self::assertEquals(true, $object->admin); + } + + public function testHydrateWithInheritance(): void + { + $expected = new ParentDto( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $event = $this->hydrator->hydrate( + ParentDto::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithHydratorAwareNormalizer(): void + { + $expected = new ProfileCreatedWrapper( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + ); + + $event = $this->hydrator->hydrate( + ProfileCreatedWrapper::class, + [ + 'event' => ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithObjectToPopulate(): void + { + $id = ProfileId::fromString('1'); + $dto = new ProfileCreated($id, Email::fromString('foo@patchlevel.de')); + + $processedDto = $this->hydrator->hydrate( + $dto::class, + ['email' => 'other@patchlevel.de'], + [StackHydrator::OBJECT_TO_POPULATE => $dto], + ); + + self::assertSame($dto, $processedDto); + self::assertSame($id, $processedDto->profileId); + self::assertEquals(Email::fromString('other@patchlevel.de'), $processedDto->email); + } + + public function testHydrateWithTypeMismatch(): void + { + $this->expectException(TypeMismatch::class); + $this->expectExceptionMessage('The value could not be set because the expected type of the property "profileId" in class "Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated" does not match.'); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => null, 'email' => null], + ); + } + + public function testHydrateWithContext(): void + { + $expect = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $data = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('hydrate') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $data, + $this->callback(static fn (array $context): bool => $context['context'] === '123' + && $context[Hydrator::HYDRATOR] instanceof StackHydrator), + $this->isInstanceOf(Mapper::class), + )->willReturn($expect); + + $hydrator = $this->builder() + ->addMiddleware($middleware) + ->build(); + + $object = $hydrator->hydrate(InferNormalizerDto::class, $data, ['context' => '123']); + + self::assertEquals($expect, $object); + } + + #[RequiresPhp('>=8.5')] + public function testHydrateWithInlineNormalizer(): void + { + $expected = new ProfileCreatedWithInlineNormalizer( + ProfileId::fromString('1'), + ValueObject::fromString('foo'), + ); + + $event = $this->hydrator->hydrate( + ProfileCreatedWithInlineNormalizer::class, + ['profileId' => '1', 'valueObject' => 'foo'], + ); + + self::assertEquals($expected, $event); + } + + public function testDenormalizationFailure(): void + { + $this->expectException(DenormalizationFailure::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => 123, 'email' => 123], + ); + } + + public function testNormalizationFailure(): void + { + $this->expectException(NormalizationFailure::class); + + $this->hydrator->extract( + new WrongNormalizer(true), + ); + } + + public function testHydrateWithNormalizerInBaseClass(): void + { + $expected = new NormalizerInBaseClassDefinedDto( + StatusWithNormalizer::Draft, + new ProfileCreatedWithNormalizer( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + [StatusWithNormalizer::Draft], + [StatusWithNormalizer::Draft], + [StatusWithNormalizer::Draft], + [ + 'foo' => new Skill('php'), + 'bar' => new Skill('symfony'), + ], + [ + 'foo' => 'php', + 'bar' => 15, + 'baz' => ['test'], + ], + ); + + $event = $this->hydrator->hydrate( + NormalizerInBaseClassDefinedDto::class, + [ + 'status' => 'draft', + 'profileCreated' => ['profileId' => '1', 'email' => 'info@patchlevel.de'], + 'defaultArray' => ['draft'], + 'listArray' => ['draft'], + 'iterableArray' => ['draft'], + 'skillsHashMap' => ['foo' => ['name' => 'php'], 'bar' => ['name' => 'symfony']], + 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizer(): void + { + $expected = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $event = $this->hydrator->hydrate( + InferNormalizerDto::class, + [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizerAndNullableProperties(): void + { + $expected = new InferNormalizerWithNullableDto( + null, + null, + null, + null, + ); + + $event = $this->hydrator->hydrate( + InferNormalizerWithNullableDto::class, + [ + 'status' => null, + 'dateTimeImmutable' => null, + 'dateTime' => null, + 'dateTimeZone' => null, + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizerWitIterables(): void + { + $expected = new InferNormalizerWithIterablesDto( + [Status::Draft], + [Status::Draft], + [Status::Draft], + [ + 'foo' => Status::Draft, + 'bar' => Status::Draft, + ], + [ + 'foo' => [Status::Draft], + 'bar' => [Status::Draft], + ], + [ + 'foo' => 'php', + 'bar' => 15, + 'baz' => ['test'], + ], + [ + 'status' => Status::Draft, + 'other' => [Status::Draft], + ], + ); + + $event = $this->hydrator->hydrate( + InferNormalizerWithIterablesDto::class, + [ + 'defaultArray' => ['draft'], + 'listArray' => ['draft'], + 'iterableArray' => ['draft'], + 'hashMap' => ['foo' => 'draft', 'bar' => 'draft'], + 'nested' => ['foo' => ['draft'], 'bar' => ['draft']], + 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], + 'shapeArray' => ['status' => 'draft', 'other' => ['draft']], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithClassNormalizer(): void + { + $object = $this->hydrator->hydrate( + ProfileId::class, + 'id', + ); + + self::assertEquals(ProfileId::fromString('id'), $object); + } + + public function testHydratePrivateProperties(): void + { + $dto = $this->hydrator->hydrate( + PrivatePropertiesDto::class, + ['profileId' => '1', 'name' => 'foo', 'age' => 12], + ); + + self::assertEquals(ProfileId::fromString('1'), $dto->profileId()); + self::assertSame('foo', $dto->name()); + self::assertSame(12, $dto->age()); + } + + public function testHydratePrivatePropertiesOfParent(): void + { + $dto = $this->hydrator->hydrate( + PrivateChildDto::class, + ['email' => 'info@patchlevel.de', 'note' => 'note', 'profileId' => '1', 'name' => 'foo'], + ); + + self::assertEquals(ProfileId::fromString('1'), $dto->profileId); + self::assertEquals(Email::fromString('info@patchlevel.de'), $dto->email()); + self::assertSame('note', $dto->note()); + self::assertSame('foo', $dto->name()); + } + + #[RequiresPhp('>=8.4')] + public function testHydrateAsymmetricVisibility(): void + { + $data = ['name' => 'foo', 'age' => 12, 'note' => 'note']; + $hydrator = $this->builder()->build(); + + $dto = $hydrator->hydrate(AsymmetricVisibilityDto::class, $data); + + self::assertEquals(new AsymmetricVisibilityDto('foo', 12, 'note'), $dto); + self::assertSame($data, $hydrator->extract($dto)); + } + + public function testHydrateCoercesScalarsLikeReflection(): void + { + $dto = $this->hydrator->hydrate( + PrivatePropertiesDto::class, + ['profileId' => '1', 'name' => 'foo', 'age' => '12'], + ); + + self::assertSame(12, $dto->age()); + } + + public function testChangedClassFallsBackToReflection(): void + { + // the renamed fields change the fingerprint, like a class which changed after the warmup + $hydrator = $this->builder() + ->addMetadataEnricher(new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + foreach ($classMetadata->properties as $property) { + $property->fieldName = 'renamed_' . $property->fieldName; + } + } + }) + ->build(); + + $data = ['renamed_profileId' => '1', 'renamed_email' => 'info@patchlevel.de']; + $event = $hydrator->hydrate(ProfileCreated::class, $data); + + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertSame($data, $hydrator->extract($event)); + self::assertInstanceOf(ReflectionMapper::class, self::mappers($hydrator)[ProfileCreated::class]); + } + + #[RequiresPhp('>=8.4')] + public function testLazyHydrate(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $expected = new LazyProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $reflection = new ReflectionClass(LazyProfileCreated::class); + self::assertTrue($reflection->isUninitializedLazyObject($event)); + + $reflection->initializeLazyObject($event); + self::assertEquals($expected, $event); + } + + #[RequiresPhp('<8.4')] + public function testLazyNotSupported(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $expected = new LazyProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals($expected, $event); + } + + #[RequiresPhp('>=8.4')] + public function testLazyExtract(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $data = $this->hydrator->extract($event); + + self::assertEquals(['profileId' => '1', 'email' => 'info@patchlevel.de'], $data); + } + + public function testDecrypt(): void + { + $object = new SensitiveDataProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $encryptedPayload = ['id' => '1', 'email' => 'encrypted']; + + $cryptographer = $this->createMock(Cryptographer::class); + $cryptographer + ->expects($this->once()) + ->method('supports') + ->with('encrypted') + ->willReturn(true); + + $cryptographer + ->expects($this->once()) + ->method('decrypt') + ->with('1', 'encrypted') + ->willReturn('info@patchlevel.de'); + + $hydrator = $this->builder() + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + + $return = $hydrator->hydrate(SensitiveDataProfileCreated::class, $encryptedPayload); + + self::assertEquals($object, $return); + } + + public function testEncrypt(): void + { + $object = new SensitiveDataProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $encryptedPayload = [ + 'id' => '1', + 'email' => [ + '__enc' => 'v1', + 'data' => 'encrypted', + 'method' => 'foo', + 'iv' => 'bar', + ], + ]; + + $cryptographer = $this->createMock(Cryptographer::class); + + $cryptographer + ->expects($this->never()) + ->method('supports'); + + $cryptographer + ->expects($this->once()) + ->method('encrypt') + ->with('1', 'info@patchlevel.de') + ->willReturn([ + '__enc' => 'v1', + 'data' => 'encrypted', + 'method' => 'foo', + 'iv' => 'bar', + ]); + + $hydrator = $this->builder() + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + + $return = $hydrator->extract($object); + + self::assertSame($encryptedPayload, $return); + } + + /** + * Reads the inlining decisions of the generated mappers the hydrator uses. + * + * @return array flag name => mapped in place + */ + private static function inlined(StackHydrator $hydrator, string $class): array + { + /** @var array $flags */ + $flags = array_filter( + get_object_vars(self::generated($hydrator, $class)), + static fn (string $name): bool => preg_match('/^i\d+$/', $name) === 1, + ARRAY_FILTER_USE_KEY, + ); + + return $flags; + } + + /** The generated mapper of the class, unwrapped from the middlewares and the lazy proxy. */ + private static function generated(StackHydrator $hydrator, string $class): GeneratedMapper + { + $mapper = self::mappers($hydrator)[$class] ?? null; + + while ($mapper instanceof MiddlewareMapper || $mapper instanceof LazyMapper) { + $mapper = (new ReflectionProperty($mapper, $mapper instanceof LazyMapper ? 'mapper' : 'next'))->getValue($mapper); + } + + self::assertInstanceOf(GeneratedMapper::class, $mapper); + + return $mapper; + } + + /** @return array> */ + private static function mappers(StackHydrator $hydrator): array + { + /** @var array> $mappers */ + $mappers = (new ReflectionProperty(StackHydrator::class, 'mappers'))->getValue($hydrator); + + return $mappers; + } + + public function testGeneratedMappersAreUsed(): void + { + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + $this->hydrator->extract($this->hydrator->hydrate(ProfileCreated::class, $data)); + + $mapper = self::mappers($this->hydrator)[ProfileCreated::class]; + + self::assertInstanceOf(GeneratedMapper::class, $mapper); + self::assertStringStartsWith(MapperFiles::NAMESPACE . '\\', $mapper::class); + } + + public function testNestedObjectsAreInlinedWithoutOtherMiddlewares(): void + { + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + self::assertSame($data, $this->hydrator->extract($this->hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame( + ['i0' => true, 'i1' => true], + self::inlined($this->hydrator, NestedLifecycleDto::class), + ); + } + + public function testNestedObjectsAreInlinedWhenOtherMiddlewaresSkipThem(): void + { + $counter = new CountingMiddleware([LifecycleFixture::class => Skip::Both]); + + $hydrator = $this->builder() + ->addMiddleware($counter) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + + self::assertSame( + ['i0' => true, 'i1' => true], + self::inlined($hydrator, NestedLifecycleDto::class), + ); + self::assertSame([NestedLifecycleDto::class => 1], $counter->hydrated); + } + + public function testNestedObjectsAreMappedWithTheirChainWhenAMiddlewareRunsForThem(): void + { + // an upcaster which can not be introspected runs for every class while hydrating + $upcaster = new class implements Upcaster { + /** + * @param array $data + * @param array $context + * + * @return array + */ + public function upcast(ClassMetadata $metadata, array $data, array $context): array + { + return $data; + } + }; + + $hydrator = $this->builder() + ->useExtension(new UpcastExtension([$upcaster])) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['i0' => true, 'i1' => true], self::inlined($hydrator, NestedLifecycleDto::class)); + + // callback upcasters for other classes do not prevent inlining + $hydrator = $this->builder() + ->useExtension(new UpcastExtension([CallbackUpcaster::forClass(ProfileCreated::class, static fn (array $data): array => $data)])) + ->build(); + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame( + ['i0' => true, 'i1' => true], + self::inlined($hydrator, NestedLifecycleDto::class), + ); + } + + public function testGeneratedMapperRunsAfterOtherMiddlewares(): void + { + $counter = new CountingMiddleware(); + + $hydrator = $this->builder() + ->addMiddleware($counter) + ->build(); + + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(ProfileCreated::class, $data))); + + self::assertInstanceOf(MiddlewareMapper::class, self::mappers($hydrator)[ProfileCreated::class]); + self::generated($hydrator, ProfileCreated::class); + self::assertSame([ProfileCreated::class => 1], $counter->hydrated); + self::assertSame([ProfileCreated::class => 1], $counter->extracted); + } + + public function testNestedObjectsGoThroughTheMiddlewaresOfTheirClass(): void + { + $counter = new CountingMiddleware(); + + $hydrator = $this->builder() + ->addMiddleware($counter) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b'], ['name' => 'c']]]; + + // the chain of the nested class contains the middleware, it sees every nested object + $object = $hydrator->hydrate(NestedLifecycleDto::class, $data); + self::assertSame($data, $hydrator->extract($object)); + + self::assertSame([NestedLifecycleDto::class => 1, LifecycleFixture::class => 3], $counter->hydrated); + self::assertSame([NestedLifecycleDto::class => 1, LifecycleFixture::class => 3], $counter->extracted); + self::assertSame(['i0' => true, 'i1' => true], self::inlined($hydrator, NestedLifecycleDto::class)); + } + + public function testNestedObjectsRespectOtherMiddlewares(): void + { + $hydrator = $this->builder() + ->useExtension(new LifecycleExtension()) + ->build(); + + $object = $hydrator->hydrate( + NestedLifecycleDto::class, + ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]], + ); + + self::assertSame('a [preHydrate] [postHydrate]', $object->child->name); + self::assertSame('b [preHydrate] [postHydrate]', $object->items[0]->name); + + self::assertSame( + [ + 'child' => ['name' => 'a [preHydrate] [postHydrate] [preExtract] [postExtract]'], + 'items' => [['name' => 'b [preHydrate] [postHydrate] [preExtract] [postExtract]']], + ], + $hydrator->extract($object), + ); + } + + public function testOuterHydratorIsUsedForNestedObjects(): void + { + $outer = new class ($this->hydrator) implements Hydrator { + /** @var list */ + public array $calls = []; + + public function __construct(private readonly Hydrator $inner) + { + } + + /** + * @param class-string $class + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(string $class, mixed $data, array $context = []): object + { + $context[Hydrator::HYDRATOR] ??= $this; + $this->calls[] = 'hydrate ' . $class; + + return $this->inner->hydrate($class, $data, $context); + } + + /** @param array $context */ + public function extract(object $object, array $context = []): mixed + { + $context[Hydrator::HYDRATOR] ??= $this; + $this->calls[] = 'extract ' . $object::class; + + return $this->inner->extract($object, $context); + } + }; + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + // nested objects are mapped in place for calls of the hydrator itself + self::assertSame($data, $this->hydrator->extract($this->hydrator->hydrate(NestedLifecycleDto::class, $data))); + + // but go through the outer hydrator for its calls + self::assertSame($data, $outer->extract($outer->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame( + [ + 'hydrate ' . NestedLifecycleDto::class, + 'hydrate ' . LifecycleFixture::class, + 'hydrate ' . LifecycleFixture::class, + 'extract ' . NestedLifecycleDto::class, + 'extract ' . LifecycleFixture::class, + 'extract ' . LifecycleFixture::class, + ], + $outer->calls, + ); + } +} diff --git a/tests/Unit/Extension/Generated/MapperGeneratorTest.php b/tests/Unit/Extension/Generated/MapperGeneratorTest.php new file mode 100644 index 00000000..920efe8a --- /dev/null +++ b/tests/Unit/Extension/Generated/MapperGeneratorTest.php @@ -0,0 +1,208 @@ +metadata(ProfileCreated::class); + + $first = (new MapperGenerator())->generate($metadata, 'Foo\\Bar'); + $second = (new MapperGenerator())->generate($metadata, 'Foo\\Bar'); + + self::assertSame($first->code, $second->code); + self::assertStringContainsString('namespace Foo;', $first->code); + self::assertStringContainsString('final class Bar extends GeneratedMapper', $first->code); + self::assertStringNotContainsString('strict_types=1', $first->code); + } + + public function testNestedClasses(): void + { + $code = (new MapperGenerator())->generate( + (new AttributeMetadataFactory())->metadata(NestedLifecycleDto::class), + 'Foo\\Bar', + ); + + self::assertSame([LifecycleFixture::class], $code->nested); + } + + /** + * 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 + { + $metadataFactory = new AttributeMetadataFactory(); + + foreach (GeneratedStackHydratorTest::CLASSES as $class) { + $metadata = $metadataFactory->metadata($class); + $className = $metadata->reflection->getShortName() . 'Mapper'; + $file = __DIR__ . '/Snapshot/' . $className . '.php'; + + $code = (new MapperGenerator())->generate($metadata, 'Patchlevel\\Hydrator\\Tests\\Snapshot\\' . $className)->code; + + 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 testDefaultsWithControlCharactersAreInlinedUnchanged(): void + { + $mapper = $this->load((new AttributeMetadataFactory())->metadata(MultilineDefaultDto::class)); + + $object = $mapper->hydrate([], []); + + self::assertInstanceOf(MultilineDefaultDto::class, $object); + 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 testFailedInitializationIsRetried(): void + { + $metadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class); + $mapper = $this->load($metadata); + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + // the metadata does not match the generated code anymore + $property = $metadata->properties['email']; + $normalizer = $property->normalizer; + $property->normalizer = null; + + try { + $mapper->hydrate($data, []); + self::fail('the first call must fail'); + } catch (OutdatedGeneratedMapper) { + } + + $property->normalizer = $normalizer; + + // the failed initialization must not leave the mapper half initialized + $object = $mapper->hydrate($data, []); + + self::assertInstanceOf(ProfileCreated::class, $object); + self::assertSame('info@patchlevel.de', $object->email->toString()); + } + + /** @param class-string $changed a class with the same properties as FingerprintDto but one changed detail */ + #[DataProvider('changedClasses')] + public function testChangedClassChangesTheFingerprint(string $changed): void + { + $metadataFactory = new AttributeMetadataFactory(); + + self::assertNotSame( + ClassFingerprint::of($metadataFactory->metadata(FingerprintDto::class)), + ClassFingerprint::of($metadataFactory->metadata($changed)), + ); + } + + /** @return iterable */ + public static function changedClasses(): iterable + { + yield 'changed default' => [FingerprintChangedDefaultDto::class]; + yield 'changed visibility' => [FingerprintChangedVisibilityDto::class]; + } + + public function testClassNormalizerIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new MapperGenerator())->generate( + (new AttributeMetadataFactory())->metadata(ProfileId::class), + 'Foo\\Bar', + ); + } + + public function testAnonymousClassIsNotGeneratable(): void + { + $object = new class { + public string $name = 'foo'; + }; + + $this->expectException(ClassNotGeneratable::class); + + (new MapperGenerator())->generate((new AttributeMetadataFactory())->metadata($object::class), 'Foo\\Bar'); + } + + /** + * @param ClassMetadata $metadata + * + * @template T of object + */ + private function load(ClassMetadata $metadata, StackHydrator|null $hydrator = null): GeneratedMapper + { + $fqcn = 'Patchlevel\\Hydrator\\Tests\\Generated\\Mapper' . self::$counter++; + $code = (new MapperGenerator())->generate($metadata, $fqcn)->code; + + $file = tempnam(sys_get_temp_dir(), 'mapper'); + assert($file !== false); + file_put_contents($file, $code); + + require $file; + unlink($file); + + assert(is_a($fqcn, GeneratedMapper::class, true)); + + return new $fqcn($metadata, $hydrator ?? new StackHydrator(), new CallStack()); + } +} diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle1DtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoMapper.php new file mode 100644 index 00000000..edcac335 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoMapper.php @@ -0,0 +1,217 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle2DtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoMapper.php new file mode 100644 index 00000000..ccb0f9fd --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoMapper.php @@ -0,0 +1,217 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle3DtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoMapper.php new file mode 100644 index 00000000..d62ad60e --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoMapper.php @@ -0,0 +1,217 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoMapper.php new file mode 100644 index 00000000..6de0b8f8 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoMapper.php @@ -0,0 +1,187 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('value'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/DefaultDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoMapper.php new file mode 100644 index 00000000..c8d31234 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoMapper.php @@ -0,0 +1,271 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + 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->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + 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->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($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, + ]; + } + + protected function initialize(): void + { + $this->d0 = $this->promotedDefault('email'); + $this->n0 = $this->normalizer('email'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoMapper.php new file mode 100644 index 00000000..505368e7 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoMapper.php @@ -0,0 +1,412 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('dateTimeImmutable'); + $this->n2 = $this->normalizer('dateTime'); + $this->n3 = $this->normalizer('dateTimeZone'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoMapper.php new file mode 100644 index 00000000..d18881a7 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoMapper.php @@ -0,0 +1,574 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('defaultArray'); + $this->n1 = $this->normalizer('listArray'); + $this->n2 = $this->normalizer('iterableArray'); + $this->n3 = $this->normalizer('hashMap'); + $this->n4 = $this->normalizer('nested'); + $this->n5 = $this->normalizer('shapeArray'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4) + || self::mayRecurse($this->n5); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoMapper.php new file mode 100644 index 00000000..891b8fd1 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoMapper.php @@ -0,0 +1,467 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('dateTimeImmutable'); + $this->n2 = $this->normalizer('dateTime'); + $this->n3 = $this->normalizer('dateTimeZone'); + $this->n4 = $this->normalizer('profileId'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedMapper.php b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedMapper.php new file mode 100644 index 00000000..ff945396 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedMapper.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoMapper.php new file mode 100644 index 00000000..b9195766 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoMapper.php @@ -0,0 +1,366 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + + $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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + + $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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('child'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + $this->n1 = $this->normalizer('items'); + $m1 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class); + $this->i1 = $m1 !== null; + if ($m1 !== null) { $this->x0 = $m1; } + if ($this->i1) { $this->n2 = $this->n1->innerNormalizer(); } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)) + || ($this->i1 ? self::reaches($this->x0) : self::mayRecurse($this->n1)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoMapper.php new file mode 100644 index 00000000..a4a620d6 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoMapper.php @@ -0,0 +1,676 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + 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->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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->n2->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::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->n3->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::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->n4->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n5->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($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->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($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->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + 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->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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->n2->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::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->n3->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::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->n4->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::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->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n5->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($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->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($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->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->i1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('profileCreated'); + $m0 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + $this->n2 = $this->normalizer('defaultArray'); + $this->n3 = $this->normalizer('listArray'); + $this->n4 = $this->normalizer('iterableArray'); + $this->n5 = $this->normalizer('skillsHashMap'); + $m1 = $this->nested($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class); + $this->i1 = $m1 !== null; + if ($m1 !== null) { $this->x1 = $m1; } + if ($this->i1) { $this->n6 = $this->n5->innerNormalizer(); } + self::initNested($this->x0 ?? null); + self::initNested($this->x1 ?? null); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n1)) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4) + || ($this->i1 ? self::reaches($this->x1) : self::mayRecurse($this->n5)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ParentDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/ParentDtoMapper.php new file mode 100644 index 00000000..5c8c3c93 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ParentDtoMapper.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoMapper.php new file mode 100644 index 00000000..1e0fb37e --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoMapper.php @@ -0,0 +1,211 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = false; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = false; + + return ($this->e)($object, $context, $inline); + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extractNested(object $object, array $context): array + { + $inline = true; + + return ($this->e)($object, $context, $inline); + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + $this->hp0 = Closure::bind(function (object $object, array $data, array $context, bool $inline): 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->ep1 = Closure::bind(function (object $object, array $context, bool $inline): 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->h = Closure::bind(function (object $object, array $data, array $context, bool $inline): object { + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'name', $e); + } + } + + ($this->hp0)($object, $data, $context, $inline); + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->e = Closure::bind(function (object $object, array $context, bool $inline): array { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class; + + try { + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoMapper.php b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoMapper.php new file mode 100644 index 00000000..9265c88f --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoMapper.php @@ -0,0 +1,176 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = false; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = false; + + return ($this->e)($object, $context, $inline); + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extractNested(object $object, array $context): array + { + $inline = true; + + return ($this->e)($object, $context, $inline); + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->h = Closure::bind(function (object $object, array $data, array $context, bool $inline): object { + 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->e = Closure::bind(function (object $object, array $context, bool $inline): array { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class; + + try { + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedMapper.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedMapper.php new file mode 100644 index 00000000..4302d55e --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedMapper.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerMapper.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerMapper.php new file mode 100644 index 00000000..b8808c31 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerMapper.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('valueObject'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperMapper.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperMapper.php new file mode 100644 index 00000000..07839025 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperMapper.php @@ -0,0 +1,217 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->hydrator; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->i0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($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->i0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileCreated'); + $m0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class); + $this->i0 = $m0 !== null; + if ($m0 !== null) { $this->x0 = $m0; } + self::initNested($this->x0 ?? null); + } + + protected function mayReachItself(): bool + { + return ($this->i0 ? self::reaches($this->x0) : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedMapper.php b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedMapper.php new file mode 100644 index 00000000..ca86e822 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedMapper.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerMapper.php b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerMapper.php new file mode 100644 index 00000000..8e0e712e --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerMapper.php @@ -0,0 +1,187 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Fixture/AsymmetricVisibilityDto.php b/tests/Unit/Fixture/AsymmetricVisibilityDto.php new file mode 100644 index 00000000..4d072150 --- /dev/null +++ b/tests/Unit/Fixture/AsymmetricVisibilityDto.php @@ -0,0 +1,16 @@ + $skip */ + public function __construct( + private readonly array $skip = [], + ) { + } + + /** @var array */ + public array $hydrated = []; + + /** @var array */ + public array $extracted = []; + + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * @param Mapper $next + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, array $context, Mapper $next): object + { + $this->hydrated[$metadata->className] = ($this->hydrated[$metadata->className] ?? 0) + 1; + + return $next->hydrate($data, $context); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * @param array $context + * @param Mapper $next + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Mapper $next): array + { + $this->extracted[$metadata->className] = ($this->extracted[$metadata->className] ?? 0) + 1; + + return $next->extract($object, $context); + } + + /** + * @param ClassMetadata $metadata + * + * @template T of object + */ + public function skip(ClassMetadata $metadata): Skip + { + return $this->skip[$metadata->className] ?? Skip::None; + } +} diff --git a/tests/Unit/Fixture/FingerprintChangedDefaultDto.php b/tests/Unit/Fixture/FingerprintChangedDefaultDto.php new file mode 100644 index 00000000..1f05ab67 --- /dev/null +++ b/tests/Unit/Fixture/FingerprintChangedDefaultDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php b/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php new file mode 100644 index 00000000..0d78638a --- /dev/null +++ b/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/FingerprintDto.php b/tests/Unit/Fixture/FingerprintDto.php new file mode 100644 index 00000000..5410b336 --- /dev/null +++ b/tests/Unit/Fixture/FingerprintDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/MultilineDefaultDto.php b/tests/Unit/Fixture/MultilineDefaultDto.php new file mode 100644 index 00000000..3df847f0 --- /dev/null +++ b/tests/Unit/Fixture/MultilineDefaultDto.php @@ -0,0 +1,21 @@ + $lines */ + public function __construct( + // phpcs:disable Squiz.Functions.MultiLineFunctionDeclaration.Indent -- the sniff does not understand the nowdoc body + public string $name = <<<'TEXT' + foo bar + baz + TEXT, + // phpcs:enable + public string $text = "first\nsecond", + public array $lines = ["a\tb" => "c\nd \$e \"f\""], + ) { + } +} diff --git a/tests/Unit/Fixture/NestedLifecycleDto.php b/tests/Unit/Fixture/NestedLifecycleDto.php new file mode 100644 index 00000000..994f5d5a --- /dev/null +++ b/tests/Unit/Fixture/NestedLifecycleDto.php @@ -0,0 +1,15 @@ + $items */ + public function __construct( + public LifecycleFixture $child, + public array $items = [], + ) { + } +} diff --git a/tests/Unit/Fixture/PrivateChildDto.php b/tests/Unit/Fixture/PrivateChildDto.php new file mode 100644 index 00000000..6e3cd349 --- /dev/null +++ b/tests/Unit/Fixture/PrivateChildDto.php @@ -0,0 +1,23 @@ +name; + } +} diff --git a/tests/Unit/Fixture/PrivateParentDto.php b/tests/Unit/Fixture/PrivateParentDto.php new file mode 100644 index 00000000..fa2e6d3d --- /dev/null +++ b/tests/Unit/Fixture/PrivateParentDto.php @@ -0,0 +1,25 @@ +email; + } + + public function note(): string + { + return $this->note; + } +} diff --git a/tests/Unit/Fixture/PrivatePropertiesDto.php b/tests/Unit/Fixture/PrivatePropertiesDto.php new file mode 100644 index 00000000..8f0a735b --- /dev/null +++ b/tests/Unit/Fixture/PrivatePropertiesDto.php @@ -0,0 +1,31 @@ +profileId; + } + + public function name(): string + { + return $this->name; + } + + public function age(): int + { + return $this->age; + } +} From da2d0c5d5eb21f2273da7d2c4d9ecafdd09dadc6 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 16 Oct 2026 17:23:24 +0200 Subject: [PATCH 2/2] Keep the subjects and revs of the generated benchmarks in line with the others --- tests/Benchmark/GeneratedHydratorBench.php | 36 ++++++++++++++++- ...GeneratedHydratorWithCryptographyBench.php | 39 ++++++++++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/tests/Benchmark/GeneratedHydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php index 17da5b5d..e51b3278 100644 --- a/tests/Benchmark/GeneratedHydratorBench.php +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -45,7 +45,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(1000)] + #[Bench\Revs(5)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -58,7 +58,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(1000)] + #[Bench\Revs(5)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -104,4 +104,36 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->extract($object); + } + } } diff --git a/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php index 7febf333..cc691d08 100644 --- a/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php +++ b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php @@ -55,7 +55,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(1000)] + #[Bench\Revs(5)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -71,7 +71,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(1000)] + #[Bench\Revs(5)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -120,4 +120,39 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->hydrate( + ProfileCreated::class, + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->extract($object); + } + } }