From 1fd2f4fb61175ef9385d99c184e85d9031108388 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 6 Sep 2024 20:15:40 +0200 Subject: [PATCH 1/6] Try to reproduce issue #75 --- src/DefinitionProvider.php | 98 ++++++++++++++++--------------------- src/Issue75/.gitignore | 1 + src/Issue75/Amount.php | 12 +++++ src/Issue75/Issue75Test.php | 43 ++++++++++++++++ src/Issue75/LowerLevel.php | 13 +++++ src/Issue75/Slot.php | 13 +++++ src/Issue75/TopLevel.php | 13 +++++ 7 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 src/Issue75/.gitignore create mode 100644 src/Issue75/Amount.php create mode 100644 src/Issue75/Issue75Test.php create mode 100644 src/Issue75/LowerLevel.php create mode 100644 src/Issue75/Slot.php create mode 100644 src/Issue75/TopLevel.php diff --git a/src/DefinitionProvider.php b/src/DefinitionProvider.php index 197068b..4d646ca 100644 --- a/src/DefinitionProvider.php +++ b/src/DefinitionProvider.php @@ -4,40 +4,23 @@ namespace EventSauce\ObjectHydrator; -use ReflectionAttribute; -use ReflectionClass; -use ReflectionMethod; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionProperty; -use ReflectionUnionType; -use function array_key_exists; -use function array_reverse; -use function count; -use function is_a; - final class DefinitionProvider { /** @var array */ private array $definitionCache = []; - private DefaultCasterRepository $defaultCasters; - private KeyFormatter $keyFormatter; - private DefaultSerializerRepository $defaultSerializers; - private PropertyTypeResolver $propertyTypeResolver; private bool $serializePublicMethods; public function __construct( - DefaultCasterRepository $defaultCasterRepository = null, - KeyFormatter $keyFormatter = null, + DefaultCasterRepository $defaultCasterRepository = null, + KeyFormatter $keyFormatter = null, DefaultSerializerRepository $defaultSerializerRepository = null, - PropertyTypeResolver $propertyTypeResolver = null, - bool $serializePublicMethods = true, - ) - { + PropertyTypeResolver $propertyTypeResolver = null, + bool $serializePublicMethods = true, + ) { $this->defaultCasters = $defaultCasterRepository ?? DefaultCasterRepository::builtIn(); $this->keyFormatter = $keyFormatter ?? new KeyFormatterForSnakeCasing(); $this->defaultSerializers = $defaultSerializerRepository ?? DefaultSerializerRepository::builtIn(); @@ -57,11 +40,11 @@ public function provideDefinition(string $className): ClassHydrationDefinition public function provideHydrationDefinition(string $className): ClassHydrationDefinition { - if (array_key_exists($className, $this->definitionCache)) { + if (\array_key_exists($className, $this->definitionCache)) { return $this->definitionCache[$className]; } - $reflectionClass = new ReflectionClass($className); + $reflectionClass = new \ReflectionClass($className); $constructor = $this->resolveConstructor($reflectionClass); $classAttributes = $reflectionClass->getAttributes(); @@ -69,7 +52,7 @@ public function provideHydrationDefinition(string $className): ClassHydrationDef $definitions = []; $constructionStyle = match (true) { - $constructor instanceof ReflectionMethod => $constructor->isConstructor() ? 'new' : 'static', + $constructor instanceof \ReflectionMethod => $constructor->isConstructor() ? 'new' : 'static', ! $reflectionClass->isInstantiable() => 'none', default => 'new', }; @@ -80,8 +63,8 @@ public function provideHydrationDefinition(string $className): ClassHydrationDef $constructorName = ''; } - /** @var ReflectionParameter[] $parameters */ - $parameters = $constructor instanceof ReflectionMethod ? $constructor->getParameters() : []; + /** @var \ReflectionParameter[] $parameters */ + $parameters = $constructor instanceof \ReflectionMethod ? $constructor->getParameters() : []; foreach ($parameters as $parameter) { $accessorName = $parameter->getName(); @@ -99,14 +82,14 @@ public function provideHydrationDefinition(string $className): ClassHydrationDef $keys = $attribute->newInstance()->keys; } - if (is_a($attributeName, PropertyCaster::class, true)) { + if (\is_a($attributeName, PropertyCaster::class, true)) { $casters[] = [$attributeName, $attribute->getArguments()]; } } - if ($firstTypeName && count($casters) === 0 && $defaultCaster = $this->defaultCasters->casterFor( - $firstTypeName - )) { + if ($firstTypeName && \count($casters) === 0 && $defaultCaster = $this->defaultCasters->casterFor( + $firstTypeName + )) { $casters = [$defaultCaster]; } @@ -139,14 +122,14 @@ public function provideHydrationDefinition(string $className): ClassHydrationDef ); } - private function resolveConstructor(ReflectionClass $reflectionClass): ?ReflectionMethod + private function resolveConstructor(\ReflectionClass $reflectionClass): ?\ReflectionMethod { - $methods = $reflectionClass->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC); + $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_STATIC | \ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { $isConstructor = $method->getAttributes(Constructor::class); - if (count($isConstructor) !== 0) { + if (\count($isConstructor) !== 0) { return $method; } } @@ -154,35 +137,40 @@ private function resolveConstructor(ReflectionClass $reflectionClass): ?Reflecti return $reflectionClass->getConstructor(); } - private function stringifyConstructor(ReflectionMethod $constructor): string + private function stringifyConstructor(\ReflectionMethod $constructor): string { return $constructor->getDeclaringClass()->getName() . '::' . $constructor->getName(); } public function provideSerializationDefinition(string $className): ClassSerializationDefinition { - $reflection = new ReflectionClass($className); + $reflection = new \ReflectionClass($className); $objectSettings = $this->resolveObjectSettings($reflection); $classAttributes = $reflection->getAttributes(); $properties = []; $publicMethods = []; if ($this->serializePublicMethods) { - $publicMethods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); + $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); } foreach ($publicMethods as $method) { if ($objectSettings->serializePublicMethods === false || $method->isStatic() + || $method->isConstructor() || $method->getNumberOfParameters() !== 0 - || count($method->getAttributes(DoNotSerialize::class)) === 1) { + || \count($method->getAttributes(DoNotSerialize::class)) === 1) { continue; } $methodName = $method->getShortName(); $key = $this->keyFormatter->propertyNameToKey($methodName); - /** @var ReflectionNamedType|ReflectionUnionType $returnType */ $returnType = $method->getReturnType(); + + if ($returnType === null) { + throw new \Exception("Method {$method->class}::{$method->name} does not have a return type"); + } + $attributes = $method->getAttributes(); $typeSpecifier = $this->typeSpecifier($attributes); $properties[] = new PropertySerializationDefinition( @@ -197,12 +185,12 @@ public function provideSerializationDefinition(string $className): ClassSerializ ); } - $publicProperties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC); + $publicProperties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC); foreach ($publicProperties as $property) { if ($property->isStatic() || $objectSettings->serializePublicProperties === false - || count($property->getAttributes(DoNotSerialize::class)) === 1) { + || \count($property->getAttributes(DoNotSerialize::class)) === 1) { continue; } @@ -212,7 +200,7 @@ public function provideSerializationDefinition(string $className): ClassSerializ $serializers = $this->resolveSerializers($propertyType, $attributes); if ($property->isPromoted()) { - $serializers = array_reverse($serializers); + $serializers = \array_reverse($serializers); } $typeSpecifier = $this->typeSpecifier($attributes); @@ -239,7 +227,7 @@ public function provideSerializationDefinition(string $className): ClassSerializ } /** - * @param ReflectionAttribute[] $attributes + * @param \ReflectionAttribute[] $attributes */ private function typeSpecifier(array $attributes): ?MapToType { @@ -256,7 +244,7 @@ private function resolveSerializer(string $type, array $attributes): array { $serializers = $this->resolveSerializersFromAttributes($attributes); - if (count($serializers) === 0 && $default = $this->defaultSerializers->serializerForType($type)) { + if (\count($serializers) === 0 && $default = $this->defaultSerializers->serializerForType($type)) { $serializers[] = $default; } @@ -274,19 +262,19 @@ public function allSerializers(): array } /** - * @param ReflectionAttribute[] $attributes + * @param \ReflectionAttribute[] $attributes * * @return array, 1: array}>|array, 1: array}> */ - private function resolveSerializers(ReflectionUnionType|ReflectionNamedType $type, array $attributes): array + private function resolveSerializers(\ReflectionUnionType|\ReflectionNamedType $type, array $attributes): array { $attributeSerializer = $this->resolveSerializersFromAttributes($attributes); - if (count($attributeSerializer) !== 0) { + if (\count($attributeSerializer) !== 0) { return $attributeSerializer; } - if ($type instanceof ReflectionNamedType) { + if ($type instanceof \ReflectionNamedType) { return [$this->defaultSerializers->serializerForType($type->getName())]; } @@ -301,7 +289,7 @@ private function resolveSerializers(ReflectionUnionType|ReflectionNamedType $typ } /** - * @param ReflectionAttribute[] $attributes + * @param \ReflectionAttribute[] $attributes */ private function resolveSerializersFromAttributes(array $attributes): array { @@ -310,7 +298,7 @@ private function resolveSerializersFromAttributes(array $attributes): array foreach ($attributes as $attribute) { $name = $attribute->getName(); - if (is_a($name, PropertySerializer::class, true)) { + if (\is_a($name, PropertySerializer::class, true)) { $serializers[] = [$attribute->getName(), $attribute->getArguments()]; } } @@ -324,7 +312,7 @@ public function hasSerializerFor(string $name): bool } /** - * @param ReflectionAttribute[] $attributes + * @param \ReflectionAttribute[] $attributes * * @return array> */ @@ -334,7 +322,7 @@ private function resolveKeys(string $defaultKey, array $attributes): array } /** - * @param ReflectionAttribute[] $attributes + * @param \ReflectionAttribute[] $attributes */ private function resolveMapFrom(array $attributes): array|false { @@ -350,7 +338,7 @@ private function resolveMapFrom(array $attributes): array|false return false; } - private function resolveObjectSettings(ReflectionClass $reflection): MapperSettings + private function resolveObjectSettings(\ReflectionClass $reflection): MapperSettings { $attributes = $this->getMapperAttributes($reflection); @@ -362,9 +350,9 @@ private function resolveObjectSettings(ReflectionClass $reflection): MapperSetti } /** - * @return ReflectionAttribute[] + * @return \ReflectionAttribute[] */ - private function getMapperAttributes(ReflectionClass $reflection): array + private function getMapperAttributes(\ReflectionClass $reflection): array { $attributes = $reflection->getAttributes(MapperSettings::class); diff --git a/src/Issue75/.gitignore b/src/Issue75/.gitignore new file mode 100644 index 0000000..60edec8 --- /dev/null +++ b/src/Issue75/.gitignore @@ -0,0 +1 @@ +*Hydrator.php \ No newline at end of file diff --git a/src/Issue75/Amount.php b/src/Issue75/Amount.php new file mode 100644 index 0000000..33aac56 --- /dev/null +++ b/src/Issue75/Amount.php @@ -0,0 +1,12 @@ +exclude('*Test.php') + ->findAllNames(); + $dumper = new ObjectMapperCodeGenerator(); + $code = $dumper->dump($classes, Issue75Hydrator::class); + \file_put_contents(__DIR__ . '/Issue75Hydrator.php', $code); + + $hydrator = new Issue75Hydrator(); + $instance = $hydrator->hydrateObject(TopLevel::class, $values = [ + 'number' => 30, + 'lower' => [ + 'amount' => [ + 'amount' => 100, + ], + 'slot' => [ + 'name' => 'name', + 'value' => 'value', + ], + ], + ]); + + $serialized = $hydrator->serializeObject($instance); + + self::assertEquals($values, $serialized); + } +} diff --git a/src/Issue75/LowerLevel.php b/src/Issue75/LowerLevel.php new file mode 100644 index 0000000..b90308c --- /dev/null +++ b/src/Issue75/LowerLevel.php @@ -0,0 +1,13 @@ + Date: Fri, 6 Sep 2024 21:08:32 +0200 Subject: [PATCH 2/6] Correct class name expander for serialization. --- src/ClassExpander.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ClassExpander.php b/src/ClassExpander.php index 468533b..722c797 100644 --- a/src/ClassExpander.php +++ b/src/ClassExpander.php @@ -5,6 +5,7 @@ namespace EventSauce\ObjectHydrator; use function array_key_exists; +use function array_push; use function array_unique; use function array_values; use function class_exists; @@ -71,10 +72,11 @@ public static function expandClassesForSerialization( /** @var PropertySerializationDefinition $property */ foreach ($classDefinition->properties as $property) { - $type = $property->type; + $type = $property->propertyType->firstTypeName(); if ( ! in_array($type, $classes) && self::isClass($type)) { - $classes[] = $type; + $nested = static::expandClassesForSerialization([$type], $definitionProvider); + array_push($classes, ...$nested); } } } From 139e4ea12d26fc45fe910c498e008c230df43429 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 6 Sep 2024 21:12:19 +0200 Subject: [PATCH 3/6] Be a little bit more forgiving. --- src/DefinitionProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DefinitionProvider.php b/src/DefinitionProvider.php index 4d646ca..5247b19 100644 --- a/src/DefinitionProvider.php +++ b/src/DefinitionProvider.php @@ -168,7 +168,7 @@ public function provideSerializationDefinition(string $className): ClassSerializ $returnType = $method->getReturnType(); if ($returnType === null) { - throw new \Exception("Method {$method->class}::{$method->name} does not have a return type"); + continue; } $attributes = $method->getAttributes(); From 19ce6c3356270d81c83a0b3ee6a4c3747f3cd563 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 6 Sep 2024 21:56:54 +0200 Subject: [PATCH 4/6] Prevent duplicate classes being returned for serialization. --- src/ClassExpander.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClassExpander.php b/src/ClassExpander.php index 722c797..e6e37dc 100644 --- a/src/ClassExpander.php +++ b/src/ClassExpander.php @@ -81,6 +81,6 @@ public static function expandClassesForSerialization( } } - return $classes; + return array_unique($classes); } } From 2a8de75f97d4e937f46860448561d7973403deee Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 6 Sep 2024 21:58:12 +0200 Subject: [PATCH 5/6] Expand recursively for hydration too. --- src/ClassExpander.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ClassExpander.php b/src/ClassExpander.php index e6e37dc..4cd620a 100644 --- a/src/ClassExpander.php +++ b/src/ClassExpander.php @@ -42,12 +42,12 @@ public static function expandClassesForHydration(array $classes, DefinitionProvi $className = (string) $propertyDefinition->firstTypeName; if ( ! in_array($className, $classes) && static::isClass($className)) { - $classes[] = $className; + array_push($classes, ...static::expandClassesForSerialization([$className], $definitionProvider)); } } } - return $classes; + return array_unique($classes); } private static function isClass(string $className): bool @@ -75,8 +75,7 @@ public static function expandClassesForSerialization( $type = $property->propertyType->firstTypeName(); if ( ! in_array($type, $classes) && self::isClass($type)) { - $nested = static::expandClassesForSerialization([$type], $definitionProvider); - array_push($classes, ...$nested); + array_push($classes, ...static::expandClassesForSerialization([$type], $definitionProvider)); } } } From 57c51ab101234a51dc6081294628fbf5012eca32 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Fri, 6 Sep 2024 22:23:10 +0200 Subject: [PATCH 6/6] Incorporate array item resolving --- src/Issue75/Issue75Test.php | 4 ++++ src/Issue75/ListItem.php | 12 ++++++++++++ src/Issue75/LowerLevel.php | 4 ++++ 3 files changed, 20 insertions(+) create mode 100644 src/Issue75/ListItem.php diff --git a/src/Issue75/Issue75Test.php b/src/Issue75/Issue75Test.php index 6d3220c..4aa9510 100644 --- a/src/Issue75/Issue75Test.php +++ b/src/Issue75/Issue75Test.php @@ -33,6 +33,10 @@ public function recursing_with_hydration_and_serialization(): void 'name' => 'name', 'value' => 'value', ], + 'items' => [ + ['value' => 'one'], + ['value' => 'two'], + ], ], ]); diff --git a/src/Issue75/ListItem.php b/src/Issue75/ListItem.php new file mode 100644 index 0000000..7437552 --- /dev/null +++ b/src/Issue75/ListItem.php @@ -0,0 +1,12 @@ +