From 6a2ad46607f58c3f65e0c9cebd5a6734a149956e Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 7 May 2026 14:49:49 +0200 Subject: [PATCH 1/2] test deep clone --- composer.json | 1 + composer.lock | 89 ++++++++++- src/CoreExtension.php | 3 +- src/Middleware/SymfonyTransformMiddleware.php | 148 ++++++++++++++++++ src/StackHydrator.php | 3 +- 5 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 src/Middleware/SymfonyTransformMiddleware.php diff --git a/composer.json b/composer.json index d1e52068..722c73e3 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "psr/cache": "^2.0.0 || ^3.0.0", "psr/simple-cache": "^2.0.0 || ^3.0.0", "symfony/event-dispatcher": "^5.4.29 || ^6.4.0 || ^7.0.0 || ^8.0.0", + "symfony/polyfill-deepclone": "^1.37", "symfony/type-info": "^7.3.0 || ^8.0.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index bf3fe207..9e3f6bc2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "607eb78316379b3e1506540bda756c5c", + "content-hash": "0ef5e1539d422496a5b8b505129d1550", "packages": [ { "name": "psr/cache", @@ -370,6 +370,93 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/polyfill-deepclone", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", + "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "provide": { + "ext-deepclone": "*" + }, + "suggest": { + "ext-deepclone": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\DeepClone\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the deepclone extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "deepclone", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:03:27+00:00" + }, { "name": "symfony/type-info", "version": "v8.0.7", diff --git a/src/CoreExtension.php b/src/CoreExtension.php index 9ff16064..424bf97f 100644 --- a/src/CoreExtension.php +++ b/src/CoreExtension.php @@ -5,6 +5,7 @@ namespace Patchlevel\Hydrator; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; +use Patchlevel\Hydrator\Middleware\SymfonyTransformMiddleware; use Patchlevel\Hydrator\Middleware\TransformMiddleware; /** @experimental */ @@ -12,7 +13,7 @@ final class CoreExtension implements Extension { public function configure(StackHydratorBuilder $builder): void { - $builder->addMiddleware(new TransformMiddleware(), -64); + $builder->addMiddleware(new SymfonyTransformMiddleware(), -64); $builder->addGuesser(new BuiltInGuesser(), -64); } } diff --git a/src/Middleware/SymfonyTransformMiddleware.php b/src/Middleware/SymfonyTransformMiddleware.php new file mode 100644 index 00000000..b70dbd13 --- /dev/null +++ b/src/Middleware/SymfonyTransformMiddleware.php @@ -0,0 +1,148 @@ + */ + private array $callStack = []; + + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + { + $constructorParameters = null; + + $vars = []; + + foreach ($metadata->properties() as $propertyMetadata) { + /* + if (!array_key_exists($propertyMetadata->fieldName(), $data)) { + if (!$propertyMetadata->reflection->isPromoted()) { + continue; + } + + $constructorParameters ??= $metadata->promotedConstructorDefaults(); + + if (!array_key_exists($propertyMetadata->propertyName, $constructorParameters)) { + continue; + } + + $vars[$propertyMetadata->propertyName] = $constructorParameters[$propertyMetadata->propertyName]->getDefaultValue(); + + $propertyMetadata->setValue( + $object, + $constructorParameters[$propertyMetadata->propertyName]->getDefaultValue(), + ); + + continue; + } + */ + + if ($propertyMetadata->normalizer) { + try { + if ($propertyMetadata->normalizer instanceof NormalizerWithContext) { + /** @psalm-suppress MixedAssignment */ + $vars[$propertyMetadata->propertyName] = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName], $context); + } else { + /** @psalm-suppress MixedAssignment */ + $vars[$propertyMetadata->propertyName] = $propertyMetadata->normalizer->denormalize($data[$propertyMetadata->fieldName]); + } + } catch (Throwable $e) { + throw new DenormalizationFailure( + $metadata->className, + $propertyMetadata->propertyName, + $propertyMetadata->normalizer::class, + $e, + ); + } + } else { + $vars[$propertyMetadata->propertyName] = $data[$propertyMetadata->fieldName]; + } + } + + return deepclone_hydrate( + $context[HydratorWithContext::OBJECT_TO_POPULATE] ?? $metadata->className(), + $vars, + ); + } + + /** + * @param array $context + * + * @return array + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + { + $objectId = spl_object_id($object); + + if (array_key_exists($objectId, $this->callStack)) { + $references = array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$objectId] = $object::class; + + try { + $data = []; + + foreach ($metadata->properties as $propertyMetadata) { + if ($propertyMetadata->normalizer) { + try { + if ($propertyMetadata->normalizer instanceof NormalizerWithContext) { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + $context, + ); + } else { + /** @psalm-suppress MixedAssignment */ + $data[$propertyMetadata->fieldName] = $propertyMetadata->normalizer->normalize( + $propertyMetadata->getValue($object), + ); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure( + $object::class, + $propertyMetadata->propertyName, + $propertyMetadata->normalizer::class, + $e, + ); + } + } else { + $data[$propertyMetadata->fieldName] = $propertyMetadata->getValue($object); + } + } + } finally { + unset($this->callStack[$objectId]); + } + + return $data; + } +} diff --git a/src/StackHydrator.php b/src/StackHydrator.php index d142a394..e1974642 100644 --- a/src/StackHydrator.php +++ b/src/StackHydrator.php @@ -10,6 +10,7 @@ use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Middleware\Middleware; use Patchlevel\Hydrator\Middleware\Stack; +use Patchlevel\Hydrator\Middleware\SymfonyTransformMiddleware; use Patchlevel\Hydrator\Middleware\TransformMiddleware; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; use ReflectionClass; @@ -27,7 +28,7 @@ final class StackHydrator implements HydratorWithContext /** @param list $middlewares */ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), - private readonly array $middlewares = [new TransformMiddleware()], + private readonly array $middlewares = [new SymfonyTransformMiddleware()], private readonly bool $defaultLazy = false, ) { if ($middlewares === []) { From 5c294a60d01258126301e6c5c75fa9feb7315606 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 7 May 2026 15:06:32 +0200 Subject: [PATCH 2/2] add deepclone extension --- .github/workflows/benchmark.yml | 7 +- composer.json | 1 - composer.lock | 89 +------------------ src/Middleware/SymfonyTransformMiddleware.php | 1 + 4 files changed, 8 insertions(+), 90 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5316d72c..579bad50 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -26,7 +26,12 @@ jobs: with: coverage: "none" php-version: "${{ matrix.php-version }}" - ini-values: memory_limit=-1, opcache.enable_cli=1, opcache.jit=tracing, opcache.jit_buffer_size=64M + ini-values: memory_limit=-1, opcache.enable_cli=1, opcache.jit=tracing, opcache.jit_buffer_size=64M, extension=deepclone + tools: pie + + - name: "Install symfony/deepclone" + run: | + sudo pie install symfony/deepclone - name: "Checkout base" uses: actions/checkout@v6 diff --git a/composer.json b/composer.json index 722c73e3..d1e52068 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "psr/cache": "^2.0.0 || ^3.0.0", "psr/simple-cache": "^2.0.0 || ^3.0.0", "symfony/event-dispatcher": "^5.4.29 || ^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/polyfill-deepclone": "^1.37", "symfony/type-info": "^7.3.0 || ^8.0.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 9e3f6bc2..bf3fe207 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0ef5e1539d422496a5b8b505129d1550", + "content-hash": "607eb78316379b3e1506540bda756c5c", "packages": [ { "name": "psr/cache", @@ -370,93 +370,6 @@ ], "time": "2024-09-25T14:21:43+00:00" }, - { - "name": "symfony/polyfill-deepclone", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-deepclone.git", - "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", - "reference": "2ca9e9e75ead5174f2b44613a646bdc9338b8eb4", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "provide": { - "ext-deepclone": "*" - }, - "suggest": { - "ext-deepclone": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\DeepClone\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the deepclone extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "deepclone", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-26T13:03:27+00:00" - }, { "name": "symfony/type-info", "version": "v8.0.7", diff --git a/src/Middleware/SymfonyTransformMiddleware.php b/src/Middleware/SymfonyTransformMiddleware.php index b70dbd13..42382ace 100644 --- a/src/Middleware/SymfonyTransformMiddleware.php +++ b/src/Middleware/SymfonyTransformMiddleware.php @@ -14,6 +14,7 @@ use function array_key_exists; use function array_values; +use function deepclone_hydrate; use function spl_object_id; /** @experimental */