From 6813838029b253abff8638df826cbd3923149eba Mon Sep 17 00:00:00 2001 From: sakulb Date: Fri, 21 Feb 2025 15:57:45 +0100 Subject: [PATCH 01/13] Add support for PHP8.4 property hooks. --- src/Metadata/Metadata.php | 1 + src/Metadata/MetadataFactory.php | 45 +++++++++++++++++++++----------- src/Service/JsonDeserializer.php | 2 +- src/Service/JsonSerializer.php | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index b10e7bc..536f5d1 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -24,6 +24,7 @@ public function __construct( public ?string $persistedName = null, public ?array $discriminatorMap = null, public ?array $orderBy = null, + public bool $getterSetterStrategy = true, ) { } } diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 0b575de..3ad3b78 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -15,6 +15,7 @@ use ReflectionUnionType; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\PropertyInfo\Type; +use const PHP_VERSION; final class MetadataFactory { @@ -171,7 +172,7 @@ private function getMethodMetadata(ReflectionMethod $method, Serialize $attribut $attribute->handler, $this->resolveCustomType($attribute), $attribute->strategy, - orderBy: $attribute->orderBy + orderBy: $attribute->orderBy, ); } @@ -201,21 +202,34 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at } } } - $getter = $getterPrefix . ucfirst($property->getName()); - $declaringClass = $property->getDeclaringClass(); - if (false === $declaringClass->hasMethod($getter)) { - // fallback to "get" prefix - $getterFallback = 'get' . ucfirst($property->getName()); - if (false === $declaringClass->hasMethod($getterFallback)) { - throw new SerializerException('Getter method ' . $getter . ' or ' . $getterFallback . ' not found in ' . $declaringClass->getName() . '.'); + $getter = $setter = null; + $getterSetterStrategy = true; + if (version_compare(PHP_VERSION, '8.4.0', '>=') && $property->hasHooks()) { + $getterSetterStrategy = false; + if ($property->hasHook(\PropertyHookType::Get)) { + $getter = $property->getName(); + } + if ($property->hasHook(\PropertyHookType::Set)) { + $setter = $property->getName(); } - - $getter = $getterFallback; } - $setter = 'set' . ucfirst($property->getName()); - if (false === $declaringClass->hasMethod($setter)) { - // setter is required for deserialization only - $setter = null; + if ($getterSetterStrategy) { + $getter = $getterPrefix . ucfirst($property->getName()); + $declaringClass = $property->getDeclaringClass(); + if (false === $declaringClass->hasMethod($getter)) { + // fallback to "get" prefix + $getterFallback = 'get' . ucfirst($property->getName()); + if (false === $declaringClass->hasMethod($getterFallback)) { + throw new SerializerException('Getter method ' . $getter . ' or ' . $getterFallback . ' not found in ' . $declaringClass->getName() . '.'); + } + + $getter = $getterFallback; + } + $setter = 'set' . ucfirst($property->getName()); + if (false === $declaringClass->hasMethod($setter)) { + // setter is required for deserialization only + $setter = null; + } } return new Metadata( @@ -229,7 +243,8 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at $attribute->strategy, $attribute->persistedName, $attribute->discriminatorMap, - orderBy: $attribute->orderBy + orderBy: $attribute->orderBy, + getterSetterStrategy: $getterSetterStrategy, ); } diff --git a/src/Service/JsonDeserializer.php b/src/Service/JsonDeserializer.php index 2ac15f8..ac12225 100644 --- a/src/Service/JsonDeserializer.php +++ b/src/Service/JsonDeserializer.php @@ -93,7 +93,7 @@ private function arrayToObject(array $data, string $className): object } try { - $object->{$metadata->setter}($value); + $metadata->getterSetterStrategy ? $object->{$metadata->setter}($value) : $object->{$metadata->property} = $value; } catch (Throwable $exception) { throw new SerializerException('Unable to deserialize "' . $name . '". Check type.', 0, $exception); } diff --git a/src/Service/JsonSerializer.php b/src/Service/JsonSerializer.php index 6e4c19c..bc85a0a 100644 --- a/src/Service/JsonSerializer.php +++ b/src/Service/JsonSerializer.php @@ -78,7 +78,7 @@ private function objectToArray(object $data, SerializationContext $context): arr { $output = []; foreach ($this->metadataRegistry->get($data::class)->getAll() as $name => $metadata) { - $value = $data->{$metadata->getter}(); + $value = $metadata->getterSetterStrategy ? $data->{$metadata->getter}() : $data->{$metadata->property}; if (null === $value && !$context->shouldSerializeNull()) { continue; From 6b3605e70881ff36926cb94dad1a627a3d3efe5f Mon Sep 17 00:00:00 2001 From: sakulb Date: Fri, 26 Jul 2024 11:19:11 +0200 Subject: [PATCH 02/13] Sanitize cache keys. --- src/Metadata/MetadataRegistry.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Metadata/MetadataRegistry.php b/src/Metadata/MetadataRegistry.php index 4935b15..5e10d39 100644 --- a/src/Metadata/MetadataRegistry.php +++ b/src/Metadata/MetadataRegistry.php @@ -39,7 +39,7 @@ public function get(string $className): ClassMetadata } if (false === isset($this->metadata[$className])) { try { - $cachedItem = $this->appCache->getItem(self::CACHE_PREFIX . $className); + $cachedItem = $this->appCache->getItem($this->getCacheKey($className)); if ($cachedItem->isHit()) { $this->metadata[$className] = $cachedItem->get(); @@ -55,4 +55,9 @@ public function get(string $className): ClassMetadata return $this->metadata[$className]; } + + private function getCacheKey(string $className): string + { + return self::CACHE_PREFIX . str_replace('\\', '_', $className); + } } From af09a22588aa26e22fe28c1d2fec4f15981d8a00 Mon Sep 17 00:00:00 2001 From: sakulb Date: Fri, 21 Jun 2024 12:34:18 +0200 Subject: [PATCH 03/13] Serialize nested arrays of objects into proper types. --- src/Handler/Handlers/ObjectHandler.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Handler/Handlers/ObjectHandler.php b/src/Handler/Handlers/ObjectHandler.php index b525e72..066e165 100644 --- a/src/Handler/Handlers/ObjectHandler.php +++ b/src/Handler/Handlers/ObjectHandler.php @@ -79,6 +79,14 @@ public function deserialize(mixed $value, Metadata $metadata): object|iterable return $collection; } if (Type::BUILTIN_TYPE_ARRAY === $metadata->type) { + if ($metadata->customType || $metadata->discriminatorMap) { + $array = []; + foreach ($value as $key => $item) { + $array[$key] = $this->jsonDeserializer->fromArray($item, $this->getDeserializeCustomType($item, $metadata) ?? $metadata->type); + } + + return $array; + } return $value; } From 9fbc3aee794061fbcf11212f02cf28fa3d8bbfcf Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 09:43:59 +0100 Subject: [PATCH 04/13] Add PHP 8.4 to ci --- .github/workflows/php.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bb62ffe..21d593a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,9 +11,11 @@ jobs: matrix: include: - php-version: 8.2 - docker-image: 'anzusystems/php:3.0.0-php82-cli' + docker-image: 'anzusystems/php:4.0.0-php82-cli' - php-version: 8.3 - docker-image: 'anzusystems/php:3.0.0-php83-cli' + docker-image: 'anzusystems/php:4.0.0-php83-cli' + - php-version: 8.4 + docker-image: 'anzusystems/php:4.0.0-php84-cli' services: mysql: From 6bd723b2ce2adaf8d3f25d885d322532f183b263 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 10:01:40 +0100 Subject: [PATCH 05/13] Fixes --- Dockerfile | 2 +- composer.json | 2 +- psalm.xml | 1 + src/Handler/Handlers/ObjectHandler.php | 2 ++ src/Metadata/MetadataFactory.php | 4 ++-- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c16b04b..e4229e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM anzusystems/php:3.0.0-php83-cli +FROM anzusystems/php:4.0.0-php84-cli # ### Basic arguments and variables ARG DOCKER_USER_ID diff --git a/composer.json b/composer.json index b9b7a0d..c14220a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "symfony/test-pack": "^1.1", "symfony/uid": "^6.3|^7.0", "symplify/easy-coding-standard": "^12.0", - "vimeo/psalm": "^5.16" + "vimeo/psalm": "^6.8" }, "suggest": { "doctrine/orm": "Enable EntityIdHandler." diff --git a/psalm.xml b/psalm.xml index 8131f92..39eb56d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,7 @@ + diff --git a/src/Handler/Handlers/ObjectHandler.php b/src/Handler/Handlers/ObjectHandler.php index 066e165..1c2129e 100644 --- a/src/Handler/Handlers/ObjectHandler.php +++ b/src/Handler/Handlers/ObjectHandler.php @@ -82,11 +82,13 @@ public function deserialize(mixed $value, Metadata $metadata): object|iterable if ($metadata->customType || $metadata->discriminatorMap) { $array = []; foreach ($value as $key => $item) { + /** @psalm-suppress ArgumentTypeCoercion */ $array[$key] = $this->jsonDeserializer->fromArray($item, $this->getDeserializeCustomType($item, $metadata) ?? $metadata->type); } return $array; } + return $value; } diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 3ad3b78..dd9f814 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -131,7 +131,7 @@ private function buildMethodMetadata(ReflectionClass $reflection): array /** @var Serialize $attribute */ $attribute = $attributes[0]->newInstance(); $dataName = $attribute->serializedName - ?? lcfirst(preg_replace('~^[get|is]*(.+)~', '$1', $method->getName())) + ?? lcfirst((string) preg_replace('~^[get|is]*(.+)~', '$1', $method->getName())) ; $metadata[$dataName] = $this->getMethodMetadata($method, $attribute); } @@ -235,7 +235,7 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at return new Metadata( $type, (bool) $propertyType?->allowsNull(), - $getter, + (string) $getter, $property->getName(), $setter, $attribute->handler, From a84141b579a945a32050e3c373e91758017a0fe1 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 10:03:22 +0100 Subject: [PATCH 06/13] Fixes --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 21d593a..a44e4ff 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: matrix: include: - php-version: 8.2 - docker-image: 'anzusystems/php:4.0.0-php82-cli' + docker-image: 'anzusystems/php:3.0.0-php82-cli' - php-version: 8.3 docker-image: 'anzusystems/php:4.0.0-php83-cli' - php-version: 8.4 From 8946fe12a3eebaf9957560135e134bd511ae339c Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 10:06:02 +0100 Subject: [PATCH 07/13] Fixes --- .github/workflows/php.yml | 2 -- composer.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index a44e4ff..724609b 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,8 +10,6 @@ jobs: strategy: matrix: include: - - php-version: 8.2 - docker-image: 'anzusystems/php:3.0.0-php82-cli' - php-version: 8.3 docker-image: 'anzusystems/php:4.0.0-php83-cli' - php-version: 8.4 diff --git a/composer.json b/composer.json index c14220a..9045b3d 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": ">=8.2", + "php": ">=8.3", "ext-json": "*", "doctrine/common": "^3.3", "symfony/property-info": "^6.3|^7.0" From 4ee3b8f633b10c3b5bbcd1c76aab229fb40250f5 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 10:09:14 +0100 Subject: [PATCH 08/13] Fixes --- psalm.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/psalm.xml b/psalm.xml index 39eb56d..4621292 100644 --- a/psalm.xml +++ b/psalm.xml @@ -7,6 +7,7 @@ usePhpDocMethodsWithoutMagicCall="true" allowStringToStandInForClass="false" memoizeMethodCallResults="true" + phpVersion="8.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" From 0ca6536cf1622fbfa0504d031180ebb2bf0b9535 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 25 Feb 2025 10:13:19 +0100 Subject: [PATCH 09/13] Fixes --- psalm.xml | 14 -------------- src/Metadata/MetadataFactory.php | 2 ++ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/psalm.xml b/psalm.xml index 4621292..9f1c9bf 100644 --- a/psalm.xml +++ b/psalm.xml @@ -28,20 +28,6 @@ - - - - - - - - - - - - - - diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index dd9f814..1854804 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -206,9 +206,11 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at $getterSetterStrategy = true; if (version_compare(PHP_VERSION, '8.4.0', '>=') && $property->hasHooks()) { $getterSetterStrategy = false; + /** @psalm-suppress UndefinedClass */ if ($property->hasHook(\PropertyHookType::Get)) { $getter = $property->getName(); } + /** @psalm-suppress UndefinedClass */ if ($property->hasHook(\PropertyHookType::Set)) { $setter = $property->getName(); } From 20beb5a8f4a9a34901a07b58ee9d2a36502d01be Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 11 Mar 2025 15:40:58 +0100 Subject: [PATCH 10/13] Add support for constructor promoted properties with private set --- src/Metadata/MetadataFactory.php | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 1854804..6796c97 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -37,19 +37,21 @@ public function buildMetadata(string $className): ClassMetadata throw new SerializerException('Cannot create reflection for ' . $className, 0, $exception); } + $propertyMetadata = $this->buildPropertyMetadata($reflection); + return new ClassMetadata( array_merge( - $this->buildPropertyMetadata($reflection), + $propertyMetadata, $this->buildMethodMetadata($reflection) ), - $this->buildConstructorMetadata($reflection), + $this->buildConstructorMetadata($propertyMetadata, $reflection), ); } /** * @throws SerializerException */ - private function buildConstructorMetadata(ReflectionClass $reflection): array + private function buildConstructorMetadata(array $propertyMetadata, ReflectionClass $reflection): array { $constructorMethod = $reflection->getConstructor(); if (null === $constructorMethod) { @@ -64,12 +66,8 @@ private function buildConstructorMetadata(ReflectionClass $reflection): array $metadata = []; foreach ($constructorMethod->getParameters() as $parameter) { - if ($parameter->isDefaultValueAvailable()) { - // we will use only the required parameters - continue; - } - $attribute = $this->findRelatedClassPropertyAttribute($reflection, $parameter->getName()); + if (null === $attribute) { // accept if the constructor has a property that should not be serialized // because the object may only be used for serialization and not deserialization @@ -77,6 +75,17 @@ private function buildConstructorMetadata(ReflectionClass $reflection): array } $dataName = $attribute->serializedName ?? $parameter->getName(); + $propertyMeta = $propertyMetadata[$dataName] ?? null; + if ($parameter->isPromoted() && $propertyMeta) { + $metadata[$dataName] = $propertyMeta; + + continue; + } + + if ($parameter->isDefaultValueAvailable()) { + // we will use only the required parameters + continue; + } $metadata[$dataName] = new Metadata( (string) $parameter->getType(), @@ -89,7 +98,7 @@ private function buildConstructorMetadata(ReflectionClass $reflection): array $attribute->strategy, $attribute->persistedName, $attribute->discriminatorMap, - orderBy: $attribute->orderBy + orderBy: $attribute->orderBy, ); } @@ -215,6 +224,13 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at $setter = $property->getName(); } } + if ($property->isPublic()) { + $getterSetterStrategy = false; + $getter = $property->getName(); + if (version_compare(PHP_VERSION, '8.4.0', '>=') && false === $property->isPrivateSet()) { + $setter = $property->getName(); + } + } if ($getterSetterStrategy) { $getter = $getterPrefix . ucfirst($property->getName()); $declaringClass = $property->getDeclaringClass(); From 50961a42084de9330395ab804c38d825f7e9e805 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 11 Mar 2025 15:45:23 +0100 Subject: [PATCH 11/13] Add support for constructor promoted properties with private set --- src/OpenApi/SerializerModelDescriber.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/OpenApi/SerializerModelDescriber.php b/src/OpenApi/SerializerModelDescriber.php index a0d9269..06823e9 100644 --- a/src/OpenApi/SerializerModelDescriber.php +++ b/src/OpenApi/SerializerModelDescriber.php @@ -73,7 +73,10 @@ public function describe(Model $model, Schema $schema): void // Method docBlock description. if (null === $metadata->setter && null !== $model->getType()->getClassName()) { /** @psalm-suppress ArgumentTypeCoercion */ - $methodReflection = new ReflectionMethod($model->getType()->getClassName(), $metadata->getter); + $methodReflection = $metadata->getterSetterStrategy + ? new ReflectionMethod($model->getType()->getClassName(), $metadata->getter) + : new ReflectionProperty($model->getType()->getClassName(), $metadata->property) + ; $this->addDocBlockDescription($methodReflection, $property); } From 16aa13feb0d4cfdb7f6bbb3f86ff152fdc4c0630 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 11 Mar 2025 15:49:16 +0100 Subject: [PATCH 12/13] Add support for constructor promoted properties with private set --- src/OpenApi/SerializerModelDescriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApi/SerializerModelDescriber.php b/src/OpenApi/SerializerModelDescriber.php index 06823e9..f95d036 100644 --- a/src/OpenApi/SerializerModelDescriber.php +++ b/src/OpenApi/SerializerModelDescriber.php @@ -75,7 +75,7 @@ public function describe(Model $model, Schema $schema): void /** @psalm-suppress ArgumentTypeCoercion */ $methodReflection = $metadata->getterSetterStrategy ? new ReflectionMethod($model->getType()->getClassName(), $metadata->getter) - : new ReflectionProperty($model->getType()->getClassName(), $metadata->property) + : new ReflectionProperty($model->getType()->getClassName(), $metadata->getter) ; $this->addDocBlockDescription($methodReflection, $property); } From 0fcf55b64bd377ceb2e0fc44daf146967db25ca8 Mon Sep 17 00:00:00 2001 From: Ronald Marfoldi Date: Tue, 11 Mar 2025 15:55:24 +0100 Subject: [PATCH 13/13] Add support for constructor promoted properties with private set --- src/Metadata/MetadataFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 6796c97..9f298e0 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -226,9 +226,9 @@ private function getPropertyMetadata(ReflectionProperty $property, Serialize $at } if ($property->isPublic()) { $getterSetterStrategy = false; - $getter = $property->getName(); - if (version_compare(PHP_VERSION, '8.4.0', '>=') && false === $property->isPrivateSet()) { - $setter = $property->getName(); + $getter = $setter = $property->getName(); + if (version_compare(PHP_VERSION, '8.4.0', '>=') && $property->isPrivateSet()) { + $setter = null; } } if ($getterSetterStrategy) {