diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 8457b5eef6c3a..7a43e63c9b7a4 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -79,15 +79,15 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments'; /** - * Hashmap of field name => callable to normalize this field. + * Hashmap of field name => callable to (de)normalize this field. * * The callable is called if the field is encountered with the arguments: * - * - mixed $attributeValue value of this field - * - object $object the whole object being normalized - * - string $attributeName name of the attribute being normalized - * - string $format the requested format - * - array $context the serialization context + * - mixed $attributeValue value of this field + * - object|string $object the whole object being normalized or the object's class being denormalized + * - string $attributeName name of the attribute being (de)normalized + * - string $format the requested format + * - array $context the serialization context */ public const CALLBACKS = 'callbacks'; @@ -168,17 +168,7 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory $this->nameConverter = $nameConverter; $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - if (isset($this->defaultContext[self::CALLBACKS])) { - if (!\is_array($this->defaultContext[self::CALLBACKS])) { - throw new InvalidArgumentException(sprintf('The "%s" default context option must be an array of callables.', self::CALLBACKS)); - } - - foreach ($this->defaultContext[self::CALLBACKS] as $attribute => $callback) { - if (!\is_callable($callback)) { - throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" default context option.', $attribute, self::CALLBACKS)); - } - } - } + $this->validateCallbackContext($this->defaultContext, 'default'); if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) { throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER)); @@ -220,11 +210,11 @@ public function setCircularReferenceHandler(callable $circularReferenceHandler) } /** - * Sets normalization callbacks. + * Sets (de)normalization callbacks. * * @deprecated since Symfony 4.2 * - * @param callable[] $callbacks Help normalize the result + * @param callable[] $callbacks Help (de)normalize the result * * @return self * @@ -532,7 +522,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class)); } - return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format)); + $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format)); } } catch (\ReflectionException $e) { throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); @@ -544,7 +534,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara return null; } - return $parameterData; + return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); } /** @@ -565,4 +555,46 @@ protected function createChildContext(array $parentContext, $attribute/*, ?strin return $parentContext; } + + /** + * Validate callbacks set in context. + * + * @param string $contextType Used to specify which context is invalid in exceptions + * + * @throws InvalidArgumentException + */ + final protected function validateCallbackContext(array $context, string $contextType = ''): void + { + if (!isset($context[self::CALLBACKS])) { + return; + } + + if (!\is_array($context[self::CALLBACKS])) { + throw new InvalidArgumentException(sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); + } + + foreach ($context[self::CALLBACKS] as $attribute => $callback) { + if (!\is_callable($callback)) { + throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); + } + } + } + + /** + * Apply callbacks set in context. + * + * @param mixed $value + * @param object|string $object Can be either the object being normalizing or the object's class being denormalized + * + * @return mixed + */ + final protected function applyCallbacks($value, $object, string $attribute, ?string $format, array $context) + { + /** + * @var callable|null + */ + $callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null; + + return $callback ? $callback($value, $object, $attribute, $format, $context) : $value; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 57df0b6b8e521..388ba10373594 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -145,17 +145,7 @@ public function normalize($object, $format = null, array $context = []) $context['cache_key'] = $this->getCacheKey($format, $context); } - if (isset($context[self::CALLBACKS])) { - if (!\is_array($context[self::CALLBACKS])) { - throw new InvalidArgumentException(sprintf('The "%s" context option must be an array of callables.', self::CALLBACKS)); - } - - foreach ($context[self::CALLBACKS] as $attribute => $callback) { - if (!\is_callable($callback)) { - throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" context option.', $attribute, self::CALLBACKS)); - } - } - } + $this->validateCallbackContext($context); if ($this->isCircularReference($object, $context)) { return $this->handleCircularReference($object, $format, $context); @@ -203,13 +193,7 @@ public function normalize($object, $format = null, array $context = []) $attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context); } - /** - * @var callable|null - */ - $callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null; - if ($callback) { - $attributeValue = $callback($attributeValue, $object, $attribute, $format, $context); - } + $attributeValue = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $context); if (null !== $attributeValue && !is_scalar($attributeValue)) { $stack[$attribute] = $attributeValue; @@ -346,6 +330,8 @@ public function denormalize($data, $type, $format = null, array $context = []) $context['cache_key'] = $this->getCacheKey($format, $context); } + $this->validateCallbackContext($context); + $allowedAttributes = $this->getAllowedAttributes($type, $context, true); $normalizedData = $this->prepareForDenormalization($data); $extraAttributes = []; @@ -375,6 +361,8 @@ public function denormalize($data, $type, $format = null, array $context = []) } $value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context); + $value = $this->applyCallbacks($value, $resolvedClass, $attribute, $format, $context); + try { $this->setAttributeValue($object, $attribute, $value, $format, $context); } catch (InvalidArgumentException $e) { @@ -509,7 +497,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format); } - return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context); + $parameterData = $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context); + + return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); } /** diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php index d2290e6dda0e5..4ed3ff1c4f0a4 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php @@ -6,13 +6,34 @@ class CallbacksObject { public $bar; - public function __construct($bar = null) + /** + * @var string|null + */ + public $foo; + + public function __construct($bar = null, string $foo = null) { $this->bar = $bar; + $this->foo = $foo; } public function getBar() { return $this->bar; } + + public function setBar($bar) + { + $this->bar = $bar; + } + + public function getFoo(): ?string + { + return $this->foo; + } + + public function setFoo(?string $foo) + { + $this->foo = $foo; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php index 459b01c92408f..4a14693002bd9 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksTestTrait.php @@ -2,6 +2,9 @@ namespace Symfony\Component\Serializer\Tests\Normalizer\Features; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -12,20 +15,92 @@ trait CallbacksTestTrait { abstract protected function getNormalizerForCallbacks(): NormalizerInterface; + abstract protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): NormalizerInterface; + /** - * @dataProvider provideCallbacks + * @dataProvider provideNormalizeCallbacks */ - public function testCallbacks($callbacks, $valueBar, $result) + public function testNormalizeCallbacks($callbacks, $valueBar, $result) { $normalizer = $this->getNormalizerForCallbacks(); $obj = new CallbacksObject(); $obj->bar = $valueBar; - $this->assertEquals( - $result, - $normalizer->normalize($obj, 'any', ['callbacks' => $callbacks]) - ); + $this->assertSame($result, $normalizer->normalize($obj, 'any', ['callbacks' => $callbacks])); + } + + /** + * @dataProvider provideNormalizeCallbacks + */ + public function testNormalizeCallbacksWithTypedProperty($callbacks, $valueBar, $result) + { + $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); + + $obj = new CallbacksObject(); + $obj->bar = $valueBar; + + $this->assertSame($result, $normalizer->normalize($obj, 'any', ['callbacks' => $callbacks])); + } + + /** + * @dataProvider provideNormalizeCallbacks + */ + public function testNormalizeCallbacksWithNoConstructorArgument($callbacks, $valueBar, $result) + { + $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); + + $obj = new class() extends CallbacksObject { + public function __construct() + { + } + }; + + $obj->bar = $valueBar; + + $this->assertSame($result, $normalizer->normalize($obj, 'any', ['callbacks' => $callbacks])); + } + + /** + * @dataProvider provideDenormalizeCallbacks + */ + public function testDenormalizeCallbacks($callbacks, $valueBar, $result) + { + $normalizer = $this->getNormalizerForCallbacks(); + + $obj = $normalizer->denormalize(['bar' => $valueBar], CallbacksObject::class, 'any', ['callbacks' => $callbacks]); + $this->assertInstanceof(CallbacksObject::class, $obj); + $this->assertEquals($result, $obj); + } + + /** + * @dataProvider providerDenormalizeCallbacksWithTypedProperty + */ + public function testDenormalizeCallbacksWithTypedProperty($callbacks, $valueBar, $result) + { + $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); + + $obj = $normalizer->denormalize(['foo' => $valueBar], CallbacksObject::class, 'any', ['callbacks' => $callbacks]); + $this->assertInstanceof(CallbacksObject::class, $obj); + $this->assertEquals($result, $obj); + } + + /** + * @dataProvider providerDenormalizeCallbacksWithTypedProperty + */ + public function testDenormalizeCallbacksWithNoConstructorArgument($callbacks, $valueBar, $result) + { + $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); + + $objWithNoConstructorArgument = new class() extends CallbacksObject { + public function __construct() + { + } + }; + + $obj = $normalizer->denormalize(['foo' => $valueBar], \get_class($objWithNoConstructorArgument), 'any', ['callbacks' => $callbacks]); + $this->assertInstanceof(\get_class($objWithNoConstructorArgument), $obj); + $this->assertEquals($result->getBar(), $obj->getBar()); } /** @@ -42,7 +117,7 @@ public function testUncallableCallbacks($callbacks) $normalizer->normalize($obj, null, ['callbacks' => $callbacks]); } - public function provideCallbacks() + public function provideNormalizeCallbacks() { return [ 'Change a string' => [ @@ -54,7 +129,7 @@ public function provideCallbacks() }, ], 'baz', - ['bar' => 'baz'], + ['bar' => 'baz', 'foo' => null], ], 'Null an item' => [ [ @@ -67,7 +142,7 @@ public function provideCallbacks() }, ], 'baz', - ['bar' => null], + ['bar' => null, 'foo' => null], ], 'Format a date' => [ [ @@ -78,7 +153,71 @@ public function provideCallbacks() }, ], new \DateTime('2011-09-10 06:30:00'), - ['bar' => '10-09-2011 06:30:00'], + ['bar' => '10-09-2011 06:30:00', 'foo' => null], + ], + 'Collect a property' => [ + [ + 'bar' => function (array $bars) { + $result = ''; + foreach ($bars as $bar) { + $result .= $bar->bar; + } + + return $result; + }, + ], + [new CallbacksObject('baz'), new CallbacksObject('quux')], + ['bar' => 'bazquux', 'foo' => null], + ], + 'Count a property' => [ + [ + 'bar' => function (array $bars) { + return \count($bars); + }, + ], + [new CallbacksObject(), new CallbacksObject()], + ['bar' => 2, 'foo' => null], + ], + ]; + } + + public function provideDenormalizeCallbacks(): array + { + return [ + 'Change a string' => [ + [ + 'bar' => function ($bar) { + $this->assertEquals('bar', $bar); + + return $bar; + }, + ], + 'bar', + new CallbacksObject('bar'), + ], + 'Null an item' => [ + [ + 'bar' => function ($value, $object, $attributeName, $format, $context) { + $this->assertSame('baz', $value); + $this->assertTrue(is_a($object, CallbacksObject::class, true)); + $this->assertSame('bar', $attributeName); + $this->assertSame('any', $format); + $this->assertIsArray($context); + }, + ], + 'baz', + new CallbacksObject(null), + ], + 'Format a date' => [ + [ + 'bar' => function ($bar) { + $this->assertIsString($bar); + + return \DateTime::createFromFormat('d-m-Y H:i:s', $bar); + }, + ], + '10-09-2011 06:30:00', + new CallbacksObject(new \DateTime('2011-09-10 06:30:00')), ], 'Collect a property' => [ [ @@ -92,7 +231,7 @@ public function provideCallbacks() }, ], [new CallbacksObject('baz'), new CallbacksObject('quux')], - ['bar' => 'bazquux'], + new CallbacksObject('bazquux'), ], 'Count a property' => [ [ @@ -101,7 +240,37 @@ public function provideCallbacks() }, ], [new CallbacksObject(), new CallbacksObject()], - ['bar' => 2], + new CallbacksObject(2), + ], + ]; + } + + public function providerDenormalizeCallbacksWithTypedProperty(): array + { + return [ + 'Change a typed string' => [ + [ + 'foo' => function ($foo) { + $this->assertEquals('foo', $foo); + + return $foo; + }, + ], + 'foo', + new CallbacksObject(null, 'foo'), + ], + 'Null an typed item' => [ + [ + 'foo' => function ($value, $object, $attributeName, $format, $context) { + $this->assertSame('fool', $value); + $this->assertTrue(is_a($object, CallbacksObject::class, true)); + $this->assertSame('foo', $attributeName); + $this->assertSame('any', $format); + $this->assertIsArray($context); + }, + ], + 'fool', + new CallbacksObject(null, null), ], ]; } @@ -113,4 +282,18 @@ public function provideInvalidCallbacks() [['bar' => 'thisisnotavalidfunction']], ]; } + + protected function getCallbackPropertyTypeExtractor(): PropertyInfoExtractor + { + $reflectionExtractor = new ReflectionExtractor(); + $phpDocExtractor = new PhpDocExtractor(); + + return new PropertyInfoExtractor( + [$reflectionExtractor, $phpDocExtractor], + [$reflectionExtractor, $phpDocExtractor], + [$reflectionExtractor, $phpDocExtractor], + [$reflectionExtractor, $phpDocExtractor], + [$reflectionExtractor, $phpDocExtractor] + ); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 0a542f4ea53f3..117a157f0024c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -227,6 +227,13 @@ public function testConstructorWArgWithPrivateMutator() $this->assertEquals('bar', $obj->getFoo()); } + protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): GetSetMethodNormalizer + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + return new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), $this->getCallbackPropertyTypeExtractor()); + } + protected function getNormalizerForCallbacks(): GetSetMethodNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); @@ -235,7 +242,7 @@ protected function getNormalizerForCallbacks(): GetSetMethodNormalizer } /** - * @dataProvider provideCallbacks + * @dataProvider provideNormalizeCallbacks */ public function testLegacyCallbacks($callbacks, $value, $result) { diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 50ed2ad0f0c43..c6c02bfb8568e 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -399,7 +399,7 @@ protected function getNormalizerForCallbacks(): ObjectNormalizer } /** - * @dataProvider provideCallbacks + * @dataProvider provideNormalizeCallbacks */ public function testLegacyCallbacks($callbacks, $value, $result) { @@ -422,6 +422,11 @@ public function testLegacyUncallableCallbacks($callbacks) $this->normalizer->setCallbacks($callbacks); } + protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): ObjectNormalizer + { + return new ObjectNormalizer(null, null, null, $this->getCallbackPropertyTypeExtractor()); + } + // circular reference protected function getNormalizerForCircularReference(array $defaultContext): ObjectNormalizer diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index be8b17124b592..c2c907d225faa 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -188,7 +188,7 @@ protected function getNormalizerForCallbacks(): PropertyNormalizer } /** - * @dataProvider provideCallbacks + * @dataProvider provideNormalizeCallbacks */ public function testLegacyCallbacks($callbacks, $value, $result) { @@ -212,6 +212,11 @@ public function testLegacyUncallableCallbacks($callbacks) $this->normalizer->setCallbacks($callbacks); } + protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): PropertyNormalizer + { + return new PropertyNormalizer(null, null, $this->getCallbackPropertyTypeExtractor()); + } + protected function getNormalizerForCircularReference(array $defaultContext): PropertyNormalizer { $normalizer = new PropertyNormalizer(null, null, null, null, null, $defaultContext); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy