From 70004b622c638b8f422fcb3ec7843634ac27101a Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 26 Feb 2026 11:50:11 +0100 Subject: [PATCH 1/5] clone stack --- src/MetadataHydrator.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index afc507f..93badc4 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -24,12 +24,15 @@ final class MetadataHydrator implements Hydrator /** @var array */ private array $classMetadata = []; + private readonly Stack $stack; + /** @param list $middlewares */ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), private readonly array $middlewares = [new TransformMiddleware()], private readonly bool $defaultLazy = false, ) { + $this->stack = new Stack($this->middlewares); } /** @@ -63,7 +66,7 @@ public function hydrate(string $class, mixed $data, array $context = []): object } if (PHP_VERSION_ID < 80400) { - $stack = new Stack($this->middlewares); + $stack = clone $this->stack; return $stack->next()->hydrate($metadata, $data, $context, $stack); } @@ -71,14 +74,14 @@ public function hydrate(string $class, mixed $data, array $context = []): object $lazy = $metadata->lazy ?? $this->defaultLazy; if (!$lazy) { - $stack = new Stack($this->middlewares); + $stack = clone $this->stack; return $stack->next()->hydrate($metadata, $data, $context, $stack); } return (new ReflectionClass($class))->newLazyProxy( function () use ($metadata, $data, $context): object { - $stack = new Stack($this->middlewares); + $stack = clone $this->stack; return $stack->next()->hydrate($metadata, $data, $context, $stack); }, @@ -94,7 +97,7 @@ public function extract(object $object, array $context = []): mixed return $metadata->normalizer->normalize($object, $context); } - $stack = new Stack($this->middlewares); + $stack = clone $this->stack; return $stack->next()->extract($metadata, $object, $context, $stack); } From bcdad75d74fb34db9a91b64efe3a77f89f5f4f6c Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 26 Feb 2026 11:51:44 +0100 Subject: [PATCH 2/5] improve stack performance --- src/Middleware/Stack.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Middleware/Stack.php b/src/Middleware/Stack.php index 7a47f62..3cba48d 100644 --- a/src/Middleware/Stack.php +++ b/src/Middleware/Stack.php @@ -16,14 +16,6 @@ public function __construct( public function next(): Middleware { - $next = $this->middlewares[$this->index] ?? null; - - if ($next === null) { - throw new NoMoreMiddleware(); - } - - $this->index++; - - return $next; + return $this->middlewares[$this->index++] ?? throw new NoMoreMiddleware(); } } From 6fe3412d3c644b69244bb27a460e5670785939c4 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 26 Feb 2026 11:58:24 +0100 Subject: [PATCH 3/5] early return first --- src/Normalizer/ObjectMapNormalizer.php | 16 ++++++++-------- src/Normalizer/ObjectNormalizer.php | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Normalizer/ObjectMapNormalizer.php b/src/Normalizer/ObjectMapNormalizer.php index 1f99571..7a2ff62 100644 --- a/src/Normalizer/ObjectMapNormalizer.php +++ b/src/Normalizer/ObjectMapNormalizer.php @@ -44,14 +44,14 @@ public function setHydrator(Hydrator $hydrator): void */ public function normalize(mixed $value, array $context): mixed { - if (!$this->hydrator) { - throw new MissingHydrator(); - } - if ($value === null) { return null; } + if (!$this->hydrator) { + throw new MissingHydrator(); + } + if (!is_object($value)) { throw InvalidArgument::withWrongType( sprintf('%s|null', implode('|', array_keys($this->classToTypeMap))), @@ -84,14 +84,14 @@ public function normalize(mixed $value, array $context): mixed */ public function denormalize(mixed $value, array $context): mixed { - if (!$this->hydrator) { - throw new MissingHydrator(); - } - if ($value === null) { return null; } + if (!$this->hydrator) { + throw new MissingHydrator(); + } + if (!is_array($value)) { throw InvalidArgument::withWrongType('array|null', $value); } diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index f97d213..6117f2d 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -26,14 +26,14 @@ public function __construct( /** @param array $context */ public function normalize(mixed $value, array $context): mixed { - if (!$this->hydrator) { - throw new MissingHydrator(); - } - if ($value === null) { return null; } + if (!$this->hydrator) { + throw new MissingHydrator(); + } + $className = $this->getClassName(); if (!$value instanceof $className) { @@ -46,14 +46,14 @@ public function normalize(mixed $value, array $context): mixed /** @param array $context */ public function denormalize(mixed $value, array $context): object|null { - if (!$this->hydrator) { - throw new MissingHydrator(); - } - if ($value === null) { return null; } + if (!$this->hydrator) { + throw new MissingHydrator(); + } + $className = $this->getClassName(); return $this->hydrator->hydrate($className, $value, $context); From 52a4717ad167e18824bb5a254de863bab8275209 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 26 Feb 2026 12:05:19 +0100 Subject: [PATCH 4/5] use isset in hot path --- src/Guesser/MappedGuesser.php | 7 +++---- src/MetadataHydrator.php | 2 +- src/Middleware/TransformMiddleware.php | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Guesser/MappedGuesser.php b/src/Guesser/MappedGuesser.php index a36875e..7169d1c 100644 --- a/src/Guesser/MappedGuesser.php +++ b/src/Guesser/MappedGuesser.php @@ -24,12 +24,11 @@ public function __construct(private array $map) public function guess(ObjectType $type): Normalizer|null { $className = $type->getClassName(); - if (! array_key_exists($className, $this->map)) { + + if (!isset($this->map[$className])) { return null; } - $normalizerType = $this->map[$className]; - - return new $normalizerType(); + return new $this->map[$className](); } } diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index 93badc4..af95bda 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -111,7 +111,7 @@ public function extract(object $object, array $context = []): mixed */ public function metadata(string $class): ClassMetadata { - if (array_key_exists($class, $this->classMetadata)) { + if (isset($this->classMetadata[$class])) { return $this->classMetadata[$class]; } diff --git a/src/Middleware/TransformMiddleware.php b/src/Middleware/TransformMiddleware.php index c8c44fa..33098c5 100644 --- a/src/Middleware/TransformMiddleware.php +++ b/src/Middleware/TransformMiddleware.php @@ -95,7 +95,7 @@ public function extract(ClassMetadata $metadata, object $object, array $context, { $objectId = spl_object_id($object); - if (array_key_exists($objectId, $this->callStack)) { + if (isset($this->callStack[$objectId])) { $references = array_values($this->callStack); $references[] = $object::class; From d85dc9171ad1c8ddfd741acb0315d1f4b31b0c0c Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 26 Feb 2026 12:57:17 +0100 Subject: [PATCH 5/5] optimize normalizer --- src/Guesser/MappedGuesser.php | 2 -- src/MetadataHydrator.php | 1 - src/Normalizer/EnumNormalizer.php | 25 +++++++++++++++---------- src/Normalizer/ObjectNormalizer.php | 25 +++++++++++++++---------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Guesser/MappedGuesser.php b/src/Guesser/MappedGuesser.php index 7169d1c..da1fa11 100644 --- a/src/Guesser/MappedGuesser.php +++ b/src/Guesser/MappedGuesser.php @@ -7,8 +7,6 @@ use Patchlevel\Hydrator\Normalizer\Normalizer; use Symfony\Component\TypeInfo\Type\ObjectType; -use function array_key_exists; - final readonly class MappedGuesser implements Guesser { /** @param array> $map */ diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index af95bda..ede8e20 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -14,7 +14,6 @@ use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; use ReflectionClass; -use function array_key_exists; use function is_array; use const PHP_VERSION_ID; diff --git a/src/Normalizer/EnumNormalizer.php b/src/Normalizer/EnumNormalizer.php index cf806d4..5bdb9b2 100644 --- a/src/Normalizer/EnumNormalizer.php +++ b/src/Normalizer/EnumNormalizer.php @@ -6,6 +6,7 @@ use Attribute; use BackedEnum; +use RuntimeException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BackedEnumType; use Symfony\Component\TypeInfo\Type\NullableType; @@ -30,12 +31,6 @@ public function normalize(mixed $value, array $context): mixed return null; } - $enum = $this->getEnum(); - - if (!$value instanceof $enum) { - throw InvalidArgument::withWrongType($enum . '|null', $value); - } - return $value->value; } @@ -50,10 +45,12 @@ public function denormalize(mixed $value, array $context): BackedEnum|null throw InvalidArgument::withWrongType('string|int|null', $value); } - $enum = $this->getEnum(); + if ($this->enum === null) { + throw InvalidType::missingType(); + } try { - return $enum::from($value); + return $this->enum::from($value); } catch (Throwable $error) { throw InvalidArgument::fromThrowable($error); } @@ -61,7 +58,7 @@ public function denormalize(mixed $value, array $context): BackedEnum|null public function handleType(Type|null $type): void { - if ($this->enum !== null || $type === null) { + if ($type === null) { return; } @@ -70,10 +67,18 @@ public function handleType(Type|null $type): void } if (!$type instanceof BackedEnumType) { + throw new RuntimeException(); + } + + if ($this->enum === null) { + $this->enum = $type->getClassName(); + return; } - $this->enum = $type->getClassName(); + if (!$type->isIdentifiedBy($this->enum)) { + throw new RuntimeException(); + } } /** @return class-string */ diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index 6117f2d..cb90371 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -6,6 +6,7 @@ use Attribute; use Patchlevel\Hydrator\Hydrator; +use RuntimeException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\Type\NullableType; @@ -34,12 +35,6 @@ public function normalize(mixed $value, array $context): mixed throw new MissingHydrator(); } - $className = $this->getClassName(); - - if (!$value instanceof $className) { - throw InvalidArgument::withWrongType($className . '|null', $value); - } - return $this->hydrator->extract($value, $context); } @@ -54,9 +49,11 @@ public function denormalize(mixed $value, array $context): object|null throw new MissingHydrator(); } - $className = $this->getClassName(); + if ($this->className === null) { + throw InvalidType::missingType(); + } - return $this->hydrator->hydrate($className, $value, $context); + return $this->hydrator->hydrate($this->className, $value, $context); } public function setHydrator(Hydrator $hydrator): void @@ -66,7 +63,7 @@ public function setHydrator(Hydrator $hydrator): void public function handleType(Type|null $type): void { - if ($type === null || $this->className !== null) { + if ($type === null) { return; } @@ -83,10 +80,18 @@ public function handleType(Type|null $type): void } if (!$type instanceof ObjectType) { + throw new RuntimeException(); + } + + if ($this->className === null) { + $this->className = $type->getClassName(); + return; } - $this->className = $type->getClassName(); + if (!$type->isIdentifiedBy($this->className)) { + throw new RuntimeException(); + } } /** @return class-string */