diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bb62ffe..724609b 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,10 +10,10 @@ 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: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: 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..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" @@ -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..9f1c9bf 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" @@ -24,22 +25,9 @@ + - - - - - - - - - - - - - - diff --git a/src/Handler/Handlers/ObjectHandler.php b/src/Handler/Handlers/ObjectHandler.php index b525e72..1c2129e 100644 --- a/src/Handler/Handlers/ObjectHandler.php +++ b/src/Handler/Handlers/ObjectHandler.php @@ -79,6 +79,16 @@ 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) { + /** @psalm-suppress ArgumentTypeCoercion */ + $array[$key] = $this->jsonDeserializer->fromArray($item, $this->getDeserializeCustomType($item, $metadata) ?? $metadata->type); + } + + return $array; + } + return $value; } 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..1854804 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 { @@ -130,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); } @@ -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,27 +202,42 @@ 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; + /** @psalm-suppress UndefinedClass */ + if ($property->hasHook(\PropertyHookType::Get)) { + $getter = $property->getName(); + } + /** @psalm-suppress UndefinedClass */ + 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( $type, (bool) $propertyType?->allowsNull(), - $getter, + (string) $getter, $property->getName(), $setter, $attribute->handler, @@ -229,7 +245,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/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); + } } 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;