From ae896be6f5fc500bed69997cf0f9b0e5976f2422 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 15:35:39 +0100 Subject: [PATCH 01/14] Cache resolved class names in normalizer --- CHANGELOG.md | 7 ++++ src/Normalizer/SimpleNormalizer.php | 9 ++++- tests/Normalizer/SimpleNormalizerTest.php | 40 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 406313a..0a9c069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.5.2 +===== + +* (improvement) Cache Doctrine-normalized class names in `SimpleNormalizer` to avoid repeated metadata lookups for the same object type. +* (improvement) Add test coverage to ensure class-name normalization is cached across repeated normalization calls. + + 1.5.1 ===== diff --git a/src/Normalizer/SimpleNormalizer.php b/src/Normalizer/SimpleNormalizer.php index bfc8ed2..ce4d018 100644 --- a/src/Normalizer/SimpleNormalizer.php +++ b/src/Normalizer/SimpleNormalizer.php @@ -26,6 +26,8 @@ class SimpleNormalizer { private readonly ?ClassMetadataFactory $doctrineMetadata; private const string STACK_CONTEXT = "simple-normalizer.debug-stack"; + /** @var array */ + private array $normalizedClassNames = []; /** * @param ServiceLocator $objectNormalizers @@ -158,7 +160,12 @@ private function normalizeClassName (string $className) : string return $className; } - return $this->doctrineMetadata->hasMetadataFor($className) + if (isset($this->normalizedClassNames[$className])) + { + return $this->normalizedClassNames[$className]; + } + + return $this->normalizedClassNames[$className] = $this->doctrineMetadata->hasMetadataFor($className) ? $this->doctrineMetadata->getMetadataFor($className)->getName() : $className; } diff --git a/tests/Normalizer/SimpleNormalizerTest.php b/tests/Normalizer/SimpleNormalizerTest.php index 5986dd5..81fc0f8 100644 --- a/tests/Normalizer/SimpleNormalizerTest.php +++ b/tests/Normalizer/SimpleNormalizerTest.php @@ -217,6 +217,46 @@ public function testWithEntityManagerWithMapping () : void $normalizer->normalize(new DummyVO(5)); } + /** + */ + public function testWithEntityManagerCachesNormalizedClassName () : void + { + $classMetaData = new ClassMetadata("SomeClass"); + + $metadataFactory = $this->createMock(ClassMetadataFactory::class); + $metadataFactory + ->expects(self::once()) + ->method("hasMetadataFor") + ->with(DummyVO::class) + ->willReturn(true); + + $metadataFactory + ->expects(self::once()) + ->method("getMetadataFor") + ->with(DummyVO::class) + ->willReturn($classMetaData); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); + + $locator = $this->createMock(ServiceLocator::class); + + $locator->expects(self::exactly(2)) + ->method("get") + ->with("SomeClass") + ->willReturn(new DummyVONormalizer(5)); + + $normalizer = new SimpleNormalizer( + objectNormalizers: $locator, + isDebug: true, + validJsonVerifier: new ValidJsonVerifier(), + entityManager: $entityManager, + ); + + $normalizer->normalize(new DummyVO(5)); + $normalizer->normalize(new DummyVO(5)); + } + /** * @return ServiceLocator */ From 8ed6700d0925d48a6d89de27bb3fb02a63d871ab Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 15:44:11 +0100 Subject: [PATCH 02/14] Implement stack-based context implementation This gives around 8/9% performance improvements --- CHANGELOG.md | 3 +- src/Normalizer/SimpleNormalizer.php | 103 ++++++++++++++++------------ 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9c069..c882e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -1.5.2 +1.5.2 (unreleased) ===== +* (improvement) Reduce normalization overhead by tracking the debug stack internally across recursion instead of mutating it in context on each nested value. * (improvement) Cache Doctrine-normalized class names in `SimpleNormalizer` to avoid repeated metadata lookups for the same object type. * (improvement) Add test coverage to ensure class-name normalization is cached across repeated normalization calls. diff --git a/src/Normalizer/SimpleNormalizer.php b/src/Normalizer/SimpleNormalizer.php index ce4d018..7319907 100644 --- a/src/Normalizer/SimpleNormalizer.php +++ b/src/Normalizer/SimpleNormalizer.php @@ -46,7 +46,8 @@ public function __construct ( */ public function normalize (mixed $value, array $context = []) : mixed { - $normalizedValue = $this->recursiveNormalize($value, $context); + $stack = $this->extractInitialStack($context); + $normalizedValue = $this->recursiveNormalize($value, $context, $stack); if ($this->isDebug) { @@ -60,7 +61,8 @@ public function normalize (mixed $value, array $context = []) : mixed */ public function normalizeArray (array $array, array $context = []) : array { - $normalizedValue = $this->recursiveNormalizeArray($array, $context); + $stack = $this->extractInitialStack($context); + $normalizedValue = $this->recursiveNormalizeArray($array, $context, $stack); if ($this->isDebug) { @@ -77,7 +79,8 @@ public function normalizeArray (array $array, array $context = []) : array public function normalizeMap (array $array, array $context = []) : array|\stdClass { // return stdClass if the array is empty here, as it will be automatically normalized to `{}` in JSON. - $normalizedValue = $this->recursiveNormalizeArray($array, $context) ?: new \stdClass(); + $stack = $this->extractInitialStack($context); + $normalizedValue = $this->recursiveNormalizeArray($array, $context, $stack) ?: new \stdClass(); if ($this->isDebug) { @@ -91,58 +94,62 @@ public function normalizeMap (array $array, array $context = []) : array|\stdCla * The actual normalize logic, that recursively normalizes the value. * It must never call one of the public methods above and just normalizes the value. */ - private function recursiveNormalize (mixed $value, array $context = []) : mixed + private function recursiveNormalize (mixed $value, array $context, array &$stack) : mixed { if (null === $value || \is_scalar($value)) { return $value; } - if (!isset($context[self::STACK_CONTEXT]) || !\is_array($context[self::STACK_CONTEXT])) - { - $context[self::STACK_CONTEXT] = []; - } - - $context[self::STACK_CONTEXT][] = get_debug_type($value); - - if (\is_array($value)) - { - return $this->recursiveNormalizeArray($value, $context); - } + $stack[] = get_debug_type($value); - if (\is_object($value)) + try { - // Allow empty stdClass as a way to force a JSON {} instead of an - // array which would encode to [] - if ($value instanceof \stdClass && [] === get_object_vars($value)) + if (\is_array($value)) { - return $value; + return $this->recursiveNormalizeArray($value, $context, $stack); } - try - { - $className = $this->normalizeClassName($value::class); - - $normalizer = $this->objectNormalizers->get($className); - \assert($normalizer instanceof SimpleObjectNormalizerInterface); - - return $normalizer->normalize($value, $context, $this); - } - catch (ServiceNotFoundException $exception) + if (\is_object($value)) { - throw new ObjectTypeNotSupportedException(\sprintf( - "Can't normalize type '%s' in stack %s", - get_debug_type($value), - implode(" > ", array_reverse($context[self::STACK_CONTEXT])), - ), 0, $exception); + // Allow empty stdClass as a way to force a JSON {} instead of an + // array which would encode to [] + if ($value instanceof \stdClass && [] === get_object_vars($value)) + { + return $value; + } + + try + { + $className = $this->normalizeClassName($value::class); + $normalizer = $this->objectNormalizers->get($className); + \assert($normalizer instanceof SimpleObjectNormalizerInterface); + + // Preserve debug stack visibility for custom object normalizers. + $context[self::STACK_CONTEXT] = $stack; + + return $normalizer->normalize($value, $context, $this); + } + catch (ServiceNotFoundException $exception) + { + throw new ObjectTypeNotSupportedException(\sprintf( + "Can't normalize type '%s' in stack %s", + get_debug_type($value), + implode(" > ", array_reverse($stack)), + ), 0, $exception); + } } - } - throw new UnsupportedTypeException(\sprintf( - "Can't normalize type %s in stack %s", - get_debug_type($value), - implode(" > ", array_reverse($context[self::STACK_CONTEXT])), - )); + throw new UnsupportedTypeException(\sprintf( + "Can't normalize type %s in stack %s", + get_debug_type($value), + implode(" > ", array_reverse($stack)), + )); + } + finally + { + array_pop($stack); + } } /** @@ -174,14 +181,14 @@ private function normalizeClassName (string $className) : string * The actual customized normalization logic for arrays, that recursively normalizes the value. * It must never call one of the public methods above and just normalizes the value. */ - private function recursiveNormalizeArray (array $array, array $context = []) : array + private function recursiveNormalizeArray (array $array, array $context, array &$stack) : array { $result = []; $isList = array_is_list($array); foreach ($array as $key => $value) { - $normalized = $this->recursiveNormalize($value, $context); + $normalized = $this->recursiveNormalize($value, $context, $stack); // if the array was a list and the normalized value is null, just filter it out if ($isList && null === $normalized) @@ -203,4 +210,14 @@ private function recursiveNormalizeArray (array $array, array $context = []) : a return $result; } + + /** + * @return list + */ + private function extractInitialStack (array $context) : array + { + return isset($context[self::STACK_CONTEXT]) && \is_array($context[self::STACK_CONTEXT]) + ? $context[self::STACK_CONTEXT] + : []; + } } From f5c269abc92a3f422be6cc7d8ebf87f2c1659dab Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:02:09 +0100 Subject: [PATCH 03/14] Improve performance of json verifier --- CHANGELOG.md | 2 + .../Validator/ValidJsonVerifier.php | 16 ++--- .../Validator/ValidJsonVerifierTest.php | 69 +++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 tests/Normalizer/Validator/ValidJsonVerifierTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c882e89..8323cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * (improvement) Reduce normalization overhead by tracking the debug stack internally across recursion instead of mutating it in context on each nested value. * (improvement) Cache Doctrine-normalized class names in `SimpleNormalizer` to avoid repeated metadata lookups for the same object type. * (improvement) Add test coverage to ensure class-name normalization is cached across repeated normalization calls. +* (improvement) Optimize `ValidJsonVerifier` by reusing a mutable path stack during traversal instead of allocating a new path array for each nested element. +* (improvement) Add dedicated verifier tests for deep-path reporting and first-invalid-element detection. 1.5.1 diff --git a/src/Normalizer/Validator/ValidJsonVerifier.php b/src/Normalizer/Validator/ValidJsonVerifier.php index 9fcc0bd..31adaf0 100644 --- a/src/Normalizer/Validator/ValidJsonVerifier.php +++ b/src/Normalizer/Validator/ValidJsonVerifier.php @@ -16,7 +16,8 @@ class ValidJsonVerifier */ public function ensureValidOnlyJsonTypes (mixed $value) : void { - $invalidElement = $this->findInvalidJsonElement($value); + $path = ["$"]; + $invalidElement = $this->findInvalidJsonElement($value, $path); if (null !== $invalidElement) { @@ -36,7 +37,7 @@ public function ensureValidOnlyJsonTypes (mixed $value) : void * * @return InvalidJsonElement|null returns null if everything is valid, otherwise the invalid value */ - private function findInvalidJsonElement (mixed $value, array $path = ["$"]) : ?InvalidJsonElement + private function findInvalidJsonElement (mixed $value, array &$path) : ?InvalidJsonElement { // scalars are always valid if (null === $value || \is_scalar($value)) @@ -49,17 +50,16 @@ private function findInvalidJsonElement (mixed $value, array $path = ["$"]) : ?I { return $value instanceof \stdClass && [] === get_object_vars($value) ? null - : new InvalidJsonElement($value, $path); + : new InvalidJsonElement($value, [...$path]); } if (\is_array($value)) { foreach ($value as $key => $item) { - $invalidItem = $this->findInvalidJsonElement( - $item, - [...$path, $key], - ); + $path[] = $key; + $invalidItem = $this->findInvalidJsonElement($item, $path); + array_pop($path); if (null !== $invalidItem) { @@ -70,6 +70,6 @@ private function findInvalidJsonElement (mixed $value, array $path = ["$"]) : ?I return null; } - return new InvalidJsonElement($value, $path); + return new InvalidJsonElement($value, [...$path]); } } diff --git a/tests/Normalizer/Validator/ValidJsonVerifierTest.php b/tests/Normalizer/Validator/ValidJsonVerifierTest.php new file mode 100644 index 0000000..8170d84 --- /dev/null +++ b/tests/Normalizer/Validator/ValidJsonVerifierTest.php @@ -0,0 +1,69 @@ + 42, + "bool" => true, + "nested" => [ + "null" => null, + "list" => [1, 2, 3], + ], + "emptyObject" => new \stdClass(), + ]; + + $verifier = new ValidJsonVerifier(); + $verifier->ensureValidOnlyJsonTypes($value); + self::assertTrue(true); + } + + /** + */ + public function testReportsDeepInvalidPath () : void + { + $verifier = new ValidJsonVerifier(); + + $this->expectException(IncompleteNormalizationException::class); + $this->expectExceptionMessage("Found a JSON-incompatible value when normalizing. Found 'Tests\Torr\SimpleNormalizer\Fixture\DummyVO' at path '$.nested.deeply.0', but expected only scalars, arrays and empty objects."); + + $verifier->ensureValidOnlyJsonTypes([ + "nested" => [ + "deeply" => [ + new DummyVO(42), + ], + ], + ]); + } + + /** + */ + public function testReportsFirstInvalidElement () : void + { + $verifier = new ValidJsonVerifier(); + + $this->expectException(IncompleteNormalizationException::class); + $this->expectExceptionMessage("Found a JSON-incompatible value when normalizing. Found 'Tests\Torr\SimpleNormalizer\Fixture\DummyVO' at path '$.first', but expected only scalars, arrays and empty objects."); + + $invalidStdClass = new \stdClass(); + $invalidStdClass->prop = "x"; + + $verifier->ensureValidOnlyJsonTypes([ + "first" => new DummyVO(5), + "second" => $invalidStdClass, + ]); + } +} From 7ddef46882ab40507b30cbf7f56d363803238639 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:21:02 +0100 Subject: [PATCH 04/14] Add more test cases for array normalization --- tests/Normalizer/ArrayNormalizerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Normalizer/ArrayNormalizerTest.php b/tests/Normalizer/ArrayNormalizerTest.php index e51b58d..4b9474c 100644 --- a/tests/Normalizer/ArrayNormalizerTest.php +++ b/tests/Normalizer/ArrayNormalizerTest.php @@ -45,6 +45,9 @@ public static function provideAssociativeArray () : iterable yield [["a" => 1, "b" => 2, "c" => 3, "d" => 4], ["a" => 1, "b" => 2, "c" => 3, "d" => 4]]; yield [["a" => 1, "b" => null, "c" => 3, "d" => null], ["a" => 1, "b" => null, "c" => 3, "d" => null]]; yield [["empty" => null], ["empty" => null]]; + yield [[0 => 1, 1 => null, 3 => 4], [0 => 1, 1 => null, 3 => 4]]; + yield [[1 => 1, 2 => null], [1 => 1, 2 => null]]; + yield [["01" => 1, 1 => 2], ["01" => 1, 1 => 2]]; } /** From 00d722e4b4ceaec70743268f1c17f2cc6b0cc007 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:23:49 +0100 Subject: [PATCH 05/14] Fix type issues --- src/Normalizer/SimpleNormalizer.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Normalizer/SimpleNormalizer.php b/src/Normalizer/SimpleNormalizer.php index 7319907..3b97a12 100644 --- a/src/Normalizer/SimpleNormalizer.php +++ b/src/Normalizer/SimpleNormalizer.php @@ -18,14 +18,13 @@ * The verifier is done on the top-level of every method (instead of at the point where the invalid values could occur * = the object normalizers), as this way we can provide a full path to the invalid element in the JSON. * - * @readonly - * * @final */ class SimpleNormalizer { private readonly ?ClassMetadataFactory $doctrineMetadata; private const string STACK_CONTEXT = "simple-normalizer.debug-stack"; + /** @var array */ private array $normalizedClassNames = []; @@ -216,8 +215,19 @@ private function recursiveNormalizeArray (array $array, array $context, array &$ */ private function extractInitialStack (array $context) : array { - return isset($context[self::STACK_CONTEXT]) && \is_array($context[self::STACK_CONTEXT]) - ? $context[self::STACK_CONTEXT] - : []; + if (!isset($context[self::STACK_CONTEXT]) || !\is_array($context[self::STACK_CONTEXT])) + { + return []; + } + + $stack = []; + + foreach ($context[self::STACK_CONTEXT] as $entry) + { + \assert(\is_string($entry)); + $stack[] = $entry; + } + + return $stack; } } From 1daba01d2a00ac1482363d163a8e49ddf75c2716 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:24:34 +0100 Subject: [PATCH 06/14] Fix PHPUnit notices --- tests/Normalizer/SimpleNormalizerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Normalizer/SimpleNormalizerTest.php b/tests/Normalizer/SimpleNormalizerTest.php index 81fc0f8..05555df 100644 --- a/tests/Normalizer/SimpleNormalizerTest.php +++ b/tests/Normalizer/SimpleNormalizerTest.php @@ -158,7 +158,7 @@ public function testWithEntityManagerButNoMapping () : void ->with(DummyVO::class) ->willReturn(false); - $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager = $this->createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); @@ -197,7 +197,7 @@ public function testWithEntityManagerWithMapping () : void ->with(DummyVO::class) ->willReturn($classMetaData); - $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager = $this->createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); @@ -236,7 +236,7 @@ public function testWithEntityManagerCachesNormalizedClassName () : void ->with(DummyVO::class) ->willReturn($classMetaData); - $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager = $this->createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); From ded427e6fa089b74f320c65d977df3a52479dbec Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:39:27 +0100 Subject: [PATCH 07/14] Optimize empty `stdClass` detection --- CHANGELOG.md | 1 + src/Normalizer/SimpleNormalizer.php | 2 +- src/Normalizer/Validator/ValidJsonVerifier.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8323cf9..9dd6526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * (improvement) Add test coverage to ensure class-name normalization is cached across repeated normalization calls. * (improvement) Optimize `ValidJsonVerifier` by reusing a mutable path stack during traversal instead of allocating a new path array for each nested element. * (improvement) Add dedicated verifier tests for deep-path reporting and first-invalid-element detection. +* (improvement) Optimize empty `stdClass` detection by using an `(array)` cast check instead of `get_object_vars()`. 1.5.1 diff --git a/src/Normalizer/SimpleNormalizer.php b/src/Normalizer/SimpleNormalizer.php index 3b97a12..8e7a859 100644 --- a/src/Normalizer/SimpleNormalizer.php +++ b/src/Normalizer/SimpleNormalizer.php @@ -113,7 +113,7 @@ private function recursiveNormalize (mixed $value, array $context, array &$stack { // Allow empty stdClass as a way to force a JSON {} instead of an // array which would encode to [] - if ($value instanceof \stdClass && [] === get_object_vars($value)) + if ($value instanceof \stdClass && [] === (array) $value) { return $value; } diff --git a/src/Normalizer/Validator/ValidJsonVerifier.php b/src/Normalizer/Validator/ValidJsonVerifier.php index 31adaf0..88dff2d 100644 --- a/src/Normalizer/Validator/ValidJsonVerifier.php +++ b/src/Normalizer/Validator/ValidJsonVerifier.php @@ -48,7 +48,7 @@ private function findInvalidJsonElement (mixed $value, array &$path) : ?InvalidJ // only empty stdClass objects are allowed (as they are used to serialize to `{}`) if (\is_object($value)) { - return $value instanceof \stdClass && [] === get_object_vars($value) + return $value instanceof \stdClass && [] === (array) $value ? null : new InvalidJsonElement($value, [...$path]); } From b4b22b506cae76eccc16c6f8a8391ce3ef8a54c9 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:39:33 +0100 Subject: [PATCH 08/14] Add missing normalizer test --- tests/Normalizer/ObjectNormalizationTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Normalizer/ObjectNormalizationTest.php b/tests/Normalizer/ObjectNormalizationTest.php index d4ab1f8..8f83dfc 100644 --- a/tests/Normalizer/ObjectNormalizationTest.php +++ b/tests/Normalizer/ObjectNormalizationTest.php @@ -3,6 +3,7 @@ namespace Tests\Torr\SimpleNormalizer\Normalizer; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; use Tests\Torr\SimpleNormalizer\Fixture\DummyVO; use Torr\SimpleNormalizer\Exception\ObjectTypeNotSupportedException; use Torr\SimpleNormalizer\Normalizer\SimpleNormalizer; @@ -81,4 +82,21 @@ public function testMissingNormalizer () : void $normalizer = $this->createNormalizer(); $normalizer->normalize(new DummyVO(11)); } + + /** + */ + public function testMissingNormalizerUsesGet () : void + { + $locator = $this->createMock(ServiceLocator::class); + $locator->expects(self::once()) + ->method("get") + ->with(DummyVO::class) + ->willThrowException(new \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException(DummyVO::class)); + + $normalizer = new SimpleNormalizer($locator); + + $this->expectException(ObjectTypeNotSupportedException::class); + $this->expectExceptionMessage("Can't normalize type 'Tests\Torr\SimpleNormalizer\Fixture\DummyVO' in stack Tests\Torr\SimpleNormalizer\Fixture\DummyVO"); + $normalizer->normalize(new DummyVO(11)); + } } From 08e3187a345f168839cee0d474243dc3d8d684c6 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 16:59:11 +0100 Subject: [PATCH 09/14] Add tests for autowiring --- .../DependencyInjection/BundleWiringTest.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/DependencyInjection/BundleWiringTest.php diff --git a/tests/DependencyInjection/BundleWiringTest.php b/tests/DependencyInjection/BundleWiringTest.php new file mode 100644 index 0000000..ec17a6f --- /dev/null +++ b/tests/DependencyInjection/BundleWiringTest.php @@ -0,0 +1,82 @@ +build($container); + + $container + ->register(BundleWiringDummyNormalizer::class, BundleWiringDummyNormalizer::class) + ->setAutoconfigured(true) + ->setAutowired(true); + + $container + ->register(SimpleNormalizer::class, SimpleNormalizer::class) + ->setPublic(true) + ->setArguments([ + new ServiceLocatorArgument( + new TaggedIteratorArgument( + "torr.normalizer.simple-object-normalizer", + defaultIndexMethod: "getNormalizedType", + needsIndexes: true, + ), + ), + false, + null, + null, + ]); + + $container->compile(); + + $normalizer = $container->get(SimpleNormalizer::class); + \assert($normalizer instanceof SimpleNormalizer); + + self::assertSame( + ["id" => 42], + $normalizer->normalize(new DummyVO(42)), + ); + } +} + +final class BundleWiringDummyNormalizer implements SimpleObjectNormalizerInterface +{ + /** + * @inheritDoc + */ + public function normalize (object $value, array $context, SimpleNormalizer $normalizer) : mixed + { + \assert($value instanceof DummyVO); + + return [ + "id" => $value->id, + ]; + } + + /** + * @inheritDoc + */ + public static function getNormalizedType () : string + { + return DummyVO::class; + } +} From 11fab4501c50118542276cef2893baf95051b76d Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 17:06:08 +0100 Subject: [PATCH 10/14] Add various tests --- tests/Normalizer/ObjectNormalizationTest.php | 28 ++++++++- .../ValueWithContextNormalizerTest.php | 62 +++++++++++++++++++ tests/Normalizer/SimpleNormalizerTest.php | 34 ++++++++++ .../Validator/ValidJsonVerifierTest.php | 48 ++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) diff --git a/tests/Normalizer/ObjectNormalizationTest.php b/tests/Normalizer/ObjectNormalizationTest.php index 8f83dfc..9496315 100644 --- a/tests/Normalizer/ObjectNormalizationTest.php +++ b/tests/Normalizer/ObjectNormalizationTest.php @@ -3,6 +3,7 @@ namespace Tests\Torr\SimpleNormalizer\Normalizer; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ServiceLocator; use Tests\Torr\SimpleNormalizer\Fixture\DummyVO; use Torr\SimpleNormalizer\Exception\ObjectTypeNotSupportedException; @@ -91,7 +92,7 @@ public function testMissingNormalizerUsesGet () : void $locator->expects(self::once()) ->method("get") ->with(DummyVO::class) - ->willThrowException(new \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException(DummyVO::class)); + ->willThrowException(new ServiceNotFoundException(DummyVO::class)); $normalizer = new SimpleNormalizer($locator); @@ -99,4 +100,29 @@ public function testMissingNormalizerUsesGet () : void $this->expectExceptionMessage("Can't normalize type 'Tests\Torr\SimpleNormalizer\Fixture\DummyVO' in stack Tests\Torr\SimpleNormalizer\Fixture\DummyVO"); $normalizer->normalize(new DummyVO(11)); } + + /** + */ + public function testMissingNormalizerKeepsPreviousException () : void + { + $previous = new ServiceNotFoundException(DummyVO::class); + + $locator = $this->createMock(ServiceLocator::class); + $locator->expects(self::once()) + ->method("get") + ->with(DummyVO::class) + ->willThrowException($previous); + + $normalizer = new SimpleNormalizer($locator); + + try + { + $normalizer->normalize(new DummyVO(11)); + self::fail("Expected ObjectTypeNotSupportedException to be thrown."); + } + catch (ObjectTypeNotSupportedException $exception) + { + self::assertSame($previous, $exception->getPrevious()); + } + } } diff --git a/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php b/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php index 123cab3..82da701 100644 --- a/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php +++ b/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php @@ -53,4 +53,66 @@ public function testContextPassing () : void "o" => 5, ]); } + + /** + */ + public function testEmptyWrappedContextKeepsIncomingContext () : void + { + $value = new ValueWithContext(new DummyVO(5), []); + + $dummyNormalizer = $this->createMock(SimpleObjectNormalizerInterface::class); + $dummyNormalizer + ->expects(self::once()) + ->method("normalize") + ->with( + $value->value, + [ + "test" => 123, + "simple-normalizer.debug-stack" => [ + get_debug_type($value), + DummyVO::class, + ], + ], + ); + + $normalizer = new SimpleNormalizer(new ServiceLocator([ + ValueWithContext::class => static fn () => new ValueWithContextNormalizer(), + DummyVO::class => static fn () => $dummyNormalizer, + ])); + + $normalizer->normalize($value, [ + "test" => 123, + ]); + } + + /** + */ + public function testEmptyIncomingContextUsesWrappedContext () : void + { + $value = new ValueWithContext(new DummyVO(5), [ + "o" => "hai", + ]); + + $dummyNormalizer = $this->createMock(SimpleObjectNormalizerInterface::class); + $dummyNormalizer + ->expects(self::once()) + ->method("normalize") + ->with( + $value->value, + [ + "o" => "hai", + "simple-normalizer.debug-stack" => [ + get_debug_type($value), + DummyVO::class, + ], + ], + ); + + $normalizer = new SimpleNormalizer(new ServiceLocator([ + ValueWithContext::class => static fn () => new ValueWithContextNormalizer(), + DummyVO::class => static fn () => $dummyNormalizer, + ])); + + $normalizer->normalize($value); + } } diff --git a/tests/Normalizer/SimpleNormalizerTest.php b/tests/Normalizer/SimpleNormalizerTest.php index 05555df..ab28927 100644 --- a/tests/Normalizer/SimpleNormalizerTest.php +++ b/tests/Normalizer/SimpleNormalizerTest.php @@ -257,6 +257,40 @@ public function testWithEntityManagerCachesNormalizedClassName () : void $normalizer->normalize(new DummyVO(5)); } + /** + */ + public function testWithEntityManagerCachesUnmappedClassName () : void + { + $metadataFactory = $this->createMock(ClassMetadataFactory::class); + $metadataFactory + ->expects(self::once()) + ->method("hasMetadataFor") + ->with(DummyVO::class) + ->willReturn(false); + $metadataFactory + ->expects(self::never()) + ->method("getMetadataFor"); + + $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); + + $locator = $this->createMock(ServiceLocator::class); + $locator->expects(self::exactly(2)) + ->method("get") + ->with(DummyVO::class) + ->willReturn(new DummyVONormalizer(5)); + + $normalizer = new SimpleNormalizer( + objectNormalizers: $locator, + isDebug: true, + validJsonVerifier: new ValidJsonVerifier(), + entityManager: $entityManager, + ); + + $normalizer->normalize(new DummyVO(5)); + $normalizer->normalize(new DummyVO(5)); + } + /** * @return ServiceLocator */ diff --git a/tests/Normalizer/Validator/ValidJsonVerifierTest.php b/tests/Normalizer/Validator/ValidJsonVerifierTest.php index 8170d84..73708d8 100644 --- a/tests/Normalizer/Validator/ValidJsonVerifierTest.php +++ b/tests/Normalizer/Validator/ValidJsonVerifierTest.php @@ -66,4 +66,52 @@ public function testReportsFirstInvalidElement () : void "second" => $invalidStdClass, ]); } + + /** + */ + public function testReportsMixedNumericAndStringKeyPath () : void + { + $verifier = new ValidJsonVerifier(); + + $this->expectException(IncompleteNormalizationException::class); + $this->expectExceptionMessage("Found a JSON-incompatible value when normalizing. Found 'Tests\Torr\SimpleNormalizer\Fixture\DummyVO' at path '$.outer.01.2.inner', but expected only scalars, arrays and empty objects."); + + $verifier->ensureValidOnlyJsonTypes([ + "outer" => [ + "01" => [ + 2 => [ + "inner" => new DummyVO(6), + ], + ], + ], + ]); + } + + /** + */ + public function testReportsVeryDeepPath () : void + { + $verifier = new ValidJsonVerifier(); + + $this->expectException(IncompleteNormalizationException::class); + $this->expectExceptionMessage("Found a JSON-incompatible value when normalizing. Found 'Tests\\Torr\\SimpleNormalizer\\Fixture\\DummyVO' at path '$.root.lvl1.lvl2.lvl3.lvl4.lvl5.lvl6.target', but expected only scalars, arrays and empty objects."); + + $verifier->ensureValidOnlyJsonTypes([ + "root" => [ + "lvl1" => [ + "lvl2" => [ + "lvl3" => [ + "lvl4" => [ + "lvl5" => [ + "lvl6" => [ + "target" => new DummyVO(9), + ], + ], + ], + ], + ], + ], + ], + ]); + } } From 6a68dab614c7adc056deac9131184099fcadd2bb Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 17:22:47 +0100 Subject: [PATCH 11/14] Add max depth guard --- CHANGELOG.md | 1 + src/Exception/InvalidMaxDepthException.php | 7 +++ .../NormalizationFailedException.php | 2 +- src/Normalizer/SimpleNormalizer.php | 26 ++++++++- .../Validator/ValidJsonVerifier.php | 23 ++++++-- tests/Normalizer/SimpleNormalizerTest.php | 56 +++++++++++++++++++ .../Validator/ValidJsonVerifierTest.php | 33 +++++++++-- 7 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/Exception/InvalidMaxDepthException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd6526..37291f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * (improvement) Optimize `ValidJsonVerifier` by reusing a mutable path stack during traversal instead of allocating a new path array for each nested element. * (improvement) Add dedicated verifier tests for deep-path reporting and first-invalid-element detection. * (improvement) Optimize empty `stdClass` detection by using an `(array)` cast check instead of `get_object_vars()`. +* (improvement) Add a default max-depth guard (128) for normalization and JSON verification to mitigate deep-nesting DoS risk. 1.5.1 diff --git a/src/Exception/InvalidMaxDepthException.php b/src/Exception/InvalidMaxDepthException.php new file mode 100644 index 0000000..285cf97 --- /dev/null +++ b/src/Exception/InvalidMaxDepthException.php @@ -0,0 +1,7 @@ + */ private array $normalizedClassNames = []; @@ -36,8 +38,14 @@ public function __construct ( private readonly bool $isDebug = false, private readonly ?ValidJsonVerifier $validJsonVerifier = null, ?EntityManagerInterface $entityManager = null, + private readonly int $maxDepth = self::DEFAULT_MAX_DEPTH, ) { + if ($this->maxDepth < 1) + { + throw new InvalidMaxDepthException("The max depth must be at least 1."); + } + $this->doctrineMetadata = $entityManager?->getMetadataFactory(); } @@ -50,7 +58,7 @@ public function normalize (mixed $value, array $context = []) : mixed if ($this->isDebug) { - $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue); + $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue, $this->maxDepth); } return $normalizedValue; @@ -65,7 +73,7 @@ public function normalizeArray (array $array, array $context = []) : array if ($this->isDebug) { - $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue); + $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue, $this->maxDepth); } return $normalizedValue; @@ -83,7 +91,7 @@ public function normalizeMap (array $array, array $context = []) : array|\stdCla if ($this->isDebug) { - $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue); + $this->validJsonVerifier?->ensureValidOnlyJsonTypes($normalizedValue, $this->maxDepth); } return $normalizedValue; @@ -100,6 +108,18 @@ private function recursiveNormalize (mixed $value, array $context, array &$stack return $value; } + if (\count($stack) >= $this->maxDepth) + { + $extendedStack = [...$stack, get_debug_type($value)]; + + throw new UnsupportedTypeException(\sprintf( + "Maximum normalization depth of %d exceeded when normalizing type %s in stack %s", + $this->maxDepth, + get_debug_type($value), + implode(" > ", array_reverse($extendedStack)), + )); + } + $stack[] = get_debug_type($value); try diff --git a/src/Normalizer/Validator/ValidJsonVerifier.php b/src/Normalizer/Validator/ValidJsonVerifier.php index 88dff2d..557d14e 100644 --- a/src/Normalizer/Validator/ValidJsonVerifier.php +++ b/src/Normalizer/Validator/ValidJsonVerifier.php @@ -12,12 +12,14 @@ class ValidJsonVerifier { /** - * Ensures that only valid JSON types are present in the value, that means scalars, arrays and empty objects. + * Ensures that only valid JSON types are present in the value that means scalars, arrays, and empty objects. + * + * @param positive-int $maxDepth */ - public function ensureValidOnlyJsonTypes (mixed $value) : void + public function ensureValidOnlyJsonTypes (mixed $value, int $maxDepth) : void { $path = ["$"]; - $invalidElement = $this->findInvalidJsonElement($value, $path); + $invalidElement = $this->findInvalidJsonElement($value, $path, $maxDepth); if (null !== $invalidElement) { @@ -36,9 +38,20 @@ public function ensureValidOnlyJsonTypes (mixed $value) : void * (scalars, arrays or empty objects). * * @return InvalidJsonElement|null returns null if everything is valid, otherwise the invalid value + * + * @param positive-int $maxDepth */ - private function findInvalidJsonElement (mixed $value, array &$path) : ?InvalidJsonElement + private function findInvalidJsonElement (mixed $value, array &$path, int $maxDepth) : ?InvalidJsonElement { + if (\count($path) - 1 > $maxDepth) + { + throw new IncompleteNormalizationException(\sprintf( + "Maximum JSON verification depth of %d exceeded at path '%s'.", + $maxDepth, + implode(".", $path), + )); + } + // scalars are always valid if (null === $value || \is_scalar($value)) { @@ -58,7 +71,7 @@ private function findInvalidJsonElement (mixed $value, array &$path) : ?InvalidJ foreach ($value as $key => $item) { $path[] = $key; - $invalidItem = $this->findInvalidJsonElement($item, $path); + $invalidItem = $this->findInvalidJsonElement($item, $path, $maxDepth); array_pop($path); if (null !== $invalidItem) diff --git a/tests/Normalizer/SimpleNormalizerTest.php b/tests/Normalizer/SimpleNormalizerTest.php index ab28927..8fe9d90 100644 --- a/tests/Normalizer/SimpleNormalizerTest.php +++ b/tests/Normalizer/SimpleNormalizerTest.php @@ -10,7 +10,9 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Tests\Torr\SimpleNormalizer\Fixture\DummyVO; use Tests\Torr\SimpleNormalizer\Fixture\DummyVONormalizer; +use Torr\SimpleNormalizer\Exception\InvalidMaxDepthException; use Torr\SimpleNormalizer\Exception\IncompleteNormalizationException; +use Torr\SimpleNormalizer\Exception\UnsupportedTypeException; use Torr\SimpleNormalizer\Normalizer\SimpleNormalizer; use Torr\SimpleNormalizer\Normalizer\Validator\ValidJsonVerifier; @@ -88,6 +90,26 @@ public function testJsonVerifierDisabled () : void self::assertTrue(true); // Just to ensure the test runs without exceptions } + /** + */ + public function testMaxDepthIsPassedToJsonVerifier () : void + { + $verifier = $this->createMock(ValidJsonVerifier::class); + $verifier + ->expects(self::once()) + ->method("ensureValidOnlyJsonTypes") + ->with("ok", 7); + + $normalizer = new SimpleNormalizer( + objectNormalizers: $this->createNormalizerObjectNormalizers("ok"), + isDebug: true, + validJsonVerifier: $verifier, + maxDepth: 7, + ); + + $normalizer->normalize(new DummyVO(1)); + } + /** * */ @@ -291,6 +313,40 @@ public function testWithEntityManagerCachesUnmappedClassName () : void $normalizer->normalize(new DummyVO(5)); } + /** + */ + public function testMaxDepthExceeded () : void + { + $normalizer = new SimpleNormalizer( + objectNormalizers: new ServiceLocator([]), + maxDepth: 2, + ); + + $this->expectException(UnsupportedTypeException::class); + $this->expectExceptionMessage("Maximum normalization depth of 2 exceeded"); + + $normalizer->normalize([ + [ + [ + "tooDeep" => true, + ], + ], + ]); + } + + /** + */ + public function testInvalidMaxDepth () : void + { + $this->expectException(InvalidMaxDepthException::class); + $this->expectExceptionMessage("The max depth must be at least 1."); + + new SimpleNormalizer( + objectNormalizers: new ServiceLocator([]), + maxDepth: 0, + ); + } + /** * @return ServiceLocator */ diff --git a/tests/Normalizer/Validator/ValidJsonVerifierTest.php b/tests/Normalizer/Validator/ValidJsonVerifierTest.php index 73708d8..eea7a13 100644 --- a/tests/Normalizer/Validator/ValidJsonVerifierTest.php +++ b/tests/Normalizer/Validator/ValidJsonVerifierTest.php @@ -27,7 +27,7 @@ public function testAcceptsJsonCompatibleValue () : void ]; $verifier = new ValidJsonVerifier(); - $verifier->ensureValidOnlyJsonTypes($value); + $verifier->ensureValidOnlyJsonTypes($value, 128); self::assertTrue(true); } @@ -46,7 +46,7 @@ public function testReportsDeepInvalidPath () : void new DummyVO(42), ], ], - ]); + ], 128); } /** @@ -64,7 +64,7 @@ public function testReportsFirstInvalidElement () : void $verifier->ensureValidOnlyJsonTypes([ "first" => new DummyVO(5), "second" => $invalidStdClass, - ]); + ], 128); } /** @@ -84,7 +84,7 @@ public function testReportsMixedNumericAndStringKeyPath () : void ], ], ], - ]); + ], 128); } /** @@ -112,6 +112,29 @@ public function testReportsVeryDeepPath () : void ], ], ], - ]); + ], 128); } + + /** + */ + public function testMaxDepthExceeded () : void + { + $verifier = new ValidJsonVerifier(); + + $this->expectException(IncompleteNormalizationException::class); + $this->expectExceptionMessage("Maximum JSON verification depth of 3 exceeded at path '$.a.b.c.d'."); + + $verifier->ensureValidOnlyJsonTypes([ + "a" => [ + "b" => [ + "c" => [ + "d" => [ + "e" => 1, + ], + ], + ], + ], + ], 3); + } + } From 7ffdd8b457f4df0927d47defec83e42e9cb3e885 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 17:28:56 +0100 Subject: [PATCH 12/14] Add suggest for doctrine --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 74c7f9c..45afd22 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,9 @@ "phpunit/phpunit": "^13.0.5", "roave/security-advisories": "dev-latest" }, + "suggest": { + "doctrine/orm": "Can be used to normalize Doctrine entity class names (for example proxy classes to their mapped entity class)." + }, "autoload": { "psr-4": { "Torr\\SimpleNormalizer\\": "src/" From c0d2552225ca283c65a542fd318dec948f37b627 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Mon, 16 Mar 2026 17:29:30 +0100 Subject: [PATCH 13/14] Fix CI --- src/Normalizer/Validator/ValidJsonVerifier.php | 4 ++-- tests/Normalizer/SimpleNormalizerTest.php | 10 +++++----- tests/Normalizer/Validator/ValidJsonVerifierTest.php | 1 - 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Normalizer/Validator/ValidJsonVerifier.php b/src/Normalizer/Validator/ValidJsonVerifier.php index 557d14e..01d86d5 100644 --- a/src/Normalizer/Validator/ValidJsonVerifier.php +++ b/src/Normalizer/Validator/ValidJsonVerifier.php @@ -37,9 +37,9 @@ public function ensureValidOnlyJsonTypes (mixed $value, int $maxDepth) : void * Searches through the value and looks for anything that isn't valid JSON * (scalars, arrays or empty objects). * - * @return InvalidJsonElement|null returns null if everything is valid, otherwise the invalid value - * * @param positive-int $maxDepth + * + * @return InvalidJsonElement|null returns null if everything is valid, otherwise the invalid value */ private function findInvalidJsonElement (mixed $value, array &$path, int $maxDepth) : ?InvalidJsonElement { diff --git a/tests/Normalizer/SimpleNormalizerTest.php b/tests/Normalizer/SimpleNormalizerTest.php index 8fe9d90..89404ff 100644 --- a/tests/Normalizer/SimpleNormalizerTest.php +++ b/tests/Normalizer/SimpleNormalizerTest.php @@ -10,8 +10,8 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Tests\Torr\SimpleNormalizer\Fixture\DummyVO; use Tests\Torr\SimpleNormalizer\Fixture\DummyVONormalizer; -use Torr\SimpleNormalizer\Exception\InvalidMaxDepthException; use Torr\SimpleNormalizer\Exception\IncompleteNormalizationException; +use Torr\SimpleNormalizer\Exception\InvalidMaxDepthException; use Torr\SimpleNormalizer\Exception\UnsupportedTypeException; use Torr\SimpleNormalizer\Normalizer\SimpleNormalizer; use Torr\SimpleNormalizer\Normalizer\Validator\ValidJsonVerifier; @@ -180,7 +180,7 @@ public function testWithEntityManagerButNoMapping () : void ->with(DummyVO::class) ->willReturn(false); - $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager = self::createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); @@ -219,7 +219,7 @@ public function testWithEntityManagerWithMapping () : void ->with(DummyVO::class) ->willReturn($classMetaData); - $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager = self::createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); @@ -258,7 +258,7 @@ public function testWithEntityManagerCachesNormalizedClassName () : void ->with(DummyVO::class) ->willReturn($classMetaData); - $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager = self::createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); @@ -293,7 +293,7 @@ public function testWithEntityManagerCachesUnmappedClassName () : void ->expects(self::never()) ->method("getMetadataFor"); - $entityManager = $this->createStub(EntityManagerInterface::class); + $entityManager = self::createStub(EntityManagerInterface::class); $entityManager->method("getMetadataFactory")->willReturn($metadataFactory); $locator = $this->createMock(ServiceLocator::class); diff --git a/tests/Normalizer/Validator/ValidJsonVerifierTest.php b/tests/Normalizer/Validator/ValidJsonVerifierTest.php index eea7a13..1c4f7c4 100644 --- a/tests/Normalizer/Validator/ValidJsonVerifierTest.php +++ b/tests/Normalizer/Validator/ValidJsonVerifierTest.php @@ -136,5 +136,4 @@ public function testMaxDepthExceeded () : void ], ], 3); } - } From 429e8d13284a7f5347f914fe3f8a7feac1022a17 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 17 Mar 2026 14:59:16 +0100 Subject: [PATCH 14/14] Remove old debug stack context --- src/Normalizer/SimpleNormalizer.php | 31 ++----------------- .../ValueWithContextNormalizerTest.php | 12 ------- 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/src/Normalizer/SimpleNormalizer.php b/src/Normalizer/SimpleNormalizer.php index c5921b6..ab40049 100644 --- a/src/Normalizer/SimpleNormalizer.php +++ b/src/Normalizer/SimpleNormalizer.php @@ -24,7 +24,6 @@ class SimpleNormalizer { private readonly ?ClassMetadataFactory $doctrineMetadata; - private const string STACK_CONTEXT = "simple-normalizer.debug-stack"; private const int DEFAULT_MAX_DEPTH = 128; /** @var array */ @@ -53,7 +52,7 @@ public function __construct ( */ public function normalize (mixed $value, array $context = []) : mixed { - $stack = $this->extractInitialStack($context); + $stack = []; $normalizedValue = $this->recursiveNormalize($value, $context, $stack); if ($this->isDebug) @@ -68,7 +67,7 @@ public function normalize (mixed $value, array $context = []) : mixed */ public function normalizeArray (array $array, array $context = []) : array { - $stack = $this->extractInitialStack($context); + $stack = []; $normalizedValue = $this->recursiveNormalizeArray($array, $context, $stack); if ($this->isDebug) @@ -86,7 +85,7 @@ public function normalizeArray (array $array, array $context = []) : array public function normalizeMap (array $array, array $context = []) : array|\stdClass { // return stdClass if the array is empty here, as it will be automatically normalized to `{}` in JSON. - $stack = $this->extractInitialStack($context); + $stack = []; $normalizedValue = $this->recursiveNormalizeArray($array, $context, $stack) ?: new \stdClass(); if ($this->isDebug) @@ -144,9 +143,6 @@ private function recursiveNormalize (mixed $value, array $context, array &$stack $normalizer = $this->objectNormalizers->get($className); \assert($normalizer instanceof SimpleObjectNormalizerInterface); - // Preserve debug stack visibility for custom object normalizers. - $context[self::STACK_CONTEXT] = $stack; - return $normalizer->normalize($value, $context, $this); } catch (ServiceNotFoundException $exception) @@ -229,25 +225,4 @@ private function recursiveNormalizeArray (array $array, array $context, array &$ return $result; } - - /** - * @return list - */ - private function extractInitialStack (array $context) : array - { - if (!isset($context[self::STACK_CONTEXT]) || !\is_array($context[self::STACK_CONTEXT])) - { - return []; - } - - $stack = []; - - foreach ($context[self::STACK_CONTEXT] as $entry) - { - \assert(\is_string($entry)); - $stack[] = $entry; - } - - return $stack; - } } diff --git a/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php b/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php index 82da701..2c3ca60 100644 --- a/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php +++ b/tests/Normalizer/ObjectNormalizer/ValueWithContextNormalizerTest.php @@ -36,10 +36,6 @@ public function testContextPassing () : void [ "test" => 123, "o" => "hai", - "simple-normalizer.debug-stack" => [ - get_debug_type($value), - DummyVO::class, - ], ], ); @@ -68,10 +64,6 @@ public function testEmptyWrappedContextKeepsIncomingContext () : void $value->value, [ "test" => 123, - "simple-normalizer.debug-stack" => [ - get_debug_type($value), - DummyVO::class, - ], ], ); @@ -101,10 +93,6 @@ public function testEmptyIncomingContextUsesWrappedContext () : void $value->value, [ "o" => "hai", - "simple-normalizer.debug-stack" => [ - get_debug_type($value), - DummyVO::class, - ], ], );