diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5316d72..579bad5 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/src/CoreExtension.php b/src/CoreExtension.php index 9ff1606..424bf97 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 0000000..42382ac --- /dev/null +++ b/src/Middleware/SymfonyTransformMiddleware.php @@ -0,0 +1,149 @@ + */ + 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 d142a39..e197464 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 === []) {