diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 44ba45f581a19..a31b105884e29 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -334,7 +334,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex $params = []; foreach ($constructorParameters as $constructorParameter) { $paramName = $constructorParameter->name; - $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; + $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context); + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $attributeContext) : $paramName; $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); @@ -346,7 +347,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex $variadicParameters = []; foreach ($data[$paramName] as $parameterData) { - $variadicParameters[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); + $variadicParameters[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); } $params = array_merge($params, $variadicParameters); @@ -363,7 +364,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex // Don't run set for a parameter passed to the constructor try { - $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format); + $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); } catch (NotNormalizableValueException $exception) { if (!isset($context['not_normalizable_value_exceptions'])) { throw $exception; @@ -485,4 +486,43 @@ final protected function applyCallbacks(mixed $value, object|string $object, str return $callback ? $callback($value, $object, $attribute, $format, $context) : $value; } + + /** + * Computes the normalization context merged with current one. Metadata always wins over global context, as more specific. + * + * @internal + */ + protected function getAttributeNormalizationContext(object $object, string $attribute, array $context): array + { + if (null === $metadata = $this->getAttributeMetadata($object, $attribute)) { + return $context; + } + + return array_merge($context, $metadata->getNormalizationContextForGroups($this->getGroups($context))); + } + + /** + * Computes the denormalization context merged with current one. Metadata always wins over global context, as more specific. + * + * @internal + */ + protected function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array + { + $context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute; + + if (null === $metadata = $this->getAttributeMetadata($class, $attribute)) { + return $context; + } + + return array_merge($context, $metadata->getDenormalizationContextForGroups($this->getGroups($context))); + } + + private function getAttributeMetadata($objectOrClass, string $attribute): ?AttributeMetadataInterface + { + if (!$this->classMetadataFactory) { + return null; + } + + return $this->classMetadataFactory->getMetadataFor($objectOrClass)->getAttributesMetadata()[$attribute] ?? null; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/ContextDummyPromotedProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/ContextDummyPromotedProperties.php new file mode 100644 index 0000000000000..5aa108d1ec8b4 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/ContextDummyPromotedProperties.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations; + +use Symfony\Component\Serializer\Annotation\Context; + +/** + * @author Maxime Steinhausser + */ +class ContextDummyPromotedProperties extends ContextDummyParent +{ + public function __construct( + /** + * @Context({ "foo" = "value", "bar" = "value", "nested" = { + * "nested_key" = "nested_value", + * }, "array": { "first", "second" } }) + * @Context({ "bar" = "value_for_group_a" }, groups = "a") + */ + public $foo, + + /** + * @Context( + * normalizationContext = { "format" = "d/m/Y" }, + * denormalizationContext = { "format" = "m-d-Y H:i" }, + * groups = {"a", "b"} + * ) + */ + public $bar, + + /** + * @Context(normalizationContext={ "prop" = "dummy_value" }) + */ + public $overriddenParentProperty, + ) { + } + + /** + * @Context({ "method" = "method_with_context" }) + */ + public function getMethodWithContext() + { + return 'method_with_context'; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/ContextDummyPromotedProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/ContextDummyPromotedProperties.php new file mode 100644 index 0000000000000..5dbc7d58ec904 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/ContextDummyPromotedProperties.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes; + +use Symfony\Component\Serializer\Annotation\Context; + +/** + * @author Maxime Steinhausser + */ +class ContextDummyPromotedProperties extends ContextDummyParent +{ + public function __construct( + #[Context(['foo' => 'value', 'bar' => 'value', 'nested' => [ + 'nested_key' => 'nested_value', + ], 'array' => ['first', 'second']])] + #[Context(context: ['bar' => 'value_for_group_a'], groups: ['a'])] + public $foo, + + #[Context( + normalizationContext: ['format' => 'd/m/Y'], + denormalizationContext: ['format' => 'm-d-Y H:i'], + groups: ['a', 'b'], + )] + public $bar, + + #[Context(normalizationContext: ['prop' => 'dummy_value'])] + public $overriddenParentProperty, + ) { + } + + #[Context(['method' => 'method_with_context'])] + public function getMethodWithContext() + { + return 'method_with_context'; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index 9245e1dcdee38..f349e9ece72fe 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -120,6 +120,14 @@ public function testLoadContexts() $this->assertLoadedContexts($this->getNamespace().'\ContextDummy', $this->getNamespace().'\ContextDummyParent'); } + /** + * @requires PHP 8 + */ + public function testLoadContextsPropertiesPromoted() + { + $this->assertLoadedContexts($this->getNamespace().'\ContextDummyPromotedProperties', $this->getNamespace().'\ContextDummyParent'); + } + public function testThrowsOnContextOnInvalidMethod() { $class = $this->getNamespace().'\BadMethodContextDummy';