diff --git a/features/serializer/dynamic_groups.feature b/features/serializer/dynamic_groups.feature new file mode 100644 index 00000000000..2454d0c2418 --- /dev/null +++ b/features/serializer/dynamic_groups.feature @@ -0,0 +1,11 @@ +@!mongodb +Feature: Dynamic serialization context + In order to customize the Resource representation dynamically + As a developer + I should be able to add and remove groups + + @createSchema + Scenario: + When I add "Content-Type" header equal to "application/ld+json" + And I send a "GET" request to "/relation_group_impact_on_collections/1" + And the JSON node "related.title" should be equal to "foo" diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 3026aa66314..52c79f13c1f 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -507,18 +507,17 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope */ protected function getFactoryOptions(array $context): array { - $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['api_normalize'] ?? ''); - if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey])) { - return $this->localFactoryOptionsCache[$operationCacheKey]; - } - $options = []; - if (isset($context[self::GROUPS])) { /* @see https://github.com/symfony/symfony/blob/v4.2.6/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php */ $options['serializer_groups'] = (array) $context[self::GROUPS]; } + $operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['api_normalize'] ?? ''); + if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey])) { + return $options + $this->localFactoryOptionsCache[$operationCacheKey]; + } + // This is a hot spot if (isset($context['resource_class'])) { // Note that the groups need to be read on the root operation @@ -536,7 +535,7 @@ protected function getFactoryOptions(array $context): array } } - return $this->localFactoryOptionsCache[$operationCacheKey] = $options; + return $options + $this->localFactoryOptionsCache[$operationCacheKey] = $options; } /** diff --git a/tests/Fixtures/TestBundle/Entity/RelationGroupImpactOnCollection.php b/tests/Fixtures/TestBundle/Entity/RelationGroupImpactOnCollection.php index 8f74d613d4a..1a9a2e9b163 100644 --- a/tests/Fixtures/TestBundle/Entity/RelationGroupImpactOnCollection.php +++ b/tests/Fixtures/TestBundle/Entity/RelationGroupImpactOnCollection.php @@ -25,6 +25,8 @@ operations: [ new GetCollection(), new Get(normalizationContext: ['groups' => 'related']), + // This adds a "related" group in the "AddGroupNormalizer" + new Get(uriTemplate: '/custom_normalizer_relation_group_impact_on_collection'), ] )] class RelationGroupImpactOnCollection diff --git a/tests/Fixtures/TestBundle/Serializer/Normalizer/AddGroupNormalizer.php b/tests/Fixtures/TestBundle/Serializer/Normalizer/AddGroupNormalizer.php new file mode 100644 index 00000000000..e9439b7d03c --- /dev/null +++ b/tests/Fixtures/TestBundle/Serializer/Normalizer/AddGroupNormalizer.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Normalizer; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationGroupImpactOnCollection; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class AddGroupNormalizer implements NormalizerAwareInterface, NormalizerInterface +{ + use NormalizerAwareTrait; + + private const ALREADY_CALLED = 'RELATED_GROUP_IMPACT_ON_COLLECTION_NORMALIZER_ALREADY_CALLED'; + + public function normalize($object, $format = null, array $context = []): array|string|int|float|bool|\ArrayObject + { + $context[self::ALREADY_CALLED] = true; + if (!($operation = $context['operation'] ?? null)) { + return $this->normalizer->normalize($object, $format, $context); + } + + if ($operation instanceof Get && '/custom_normalizer_relation_group_impact_on_collection' === $operation->getUriTemplate()) { + $context['groups'] = ['related']; + } + + return $this->normalizer->normalize($object, $format, $context); + } + + public function supportsNormalization($data, $format = null, array $context = []): bool + { + // Make sure we're not called twice + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof RelationGroupImpactOnCollection; + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 96a0a3ae8a2..98ff470df5e 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -415,3 +415,7 @@ services: tags: - { name: 'api_platform.state_processor' } + ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Normalizer\AddGroupNormalizer: + tags: + - { name: 'serializer.normalizer' } +