Search code examples
phpsymfonydecoratorapi-platform.com

After ApiPlatform migrates from 3.1 to 3.2, my Denormalizer crashes because it's sent to ValidationExceptionNormalizer


Inspired by the awesome tutorial of Ryan Weaver about ApiPlatform on SymfonyCast, I created a Normalizer and a Denormalizer to manage groups normalization and denormalization.

The GroupsDenormalizer decorates api_platform.serializer.normalizer.item The GroupsNormalizer decorates api_platform.jsonld.normalizer.item I used the #[AsDecorator] attribute to do it.

All works! My test cover 100% of code. There is no deprecations. As soon as I upgrade from ApiPlatform 3.1.20 to 3.2.0, my code fails and Api is no more available.

TypeError: ApiPlatform\Symfony\Validator\Serializer\ValidationExceptionNormalizer::__construct(): Argument #1 ($decorated) must be of type Symfony\Component\Serializer\Normalizer\NormalizerInterface, App\Security\GroupsDenormalizer given, called in /srv/app/var/cache/test/ContainerCco4qUx/App_KernelTestDebugContainer.php on line 2484

I don't understand why dependency injection sent my denormalizer as argument of ValidationExceptionNormalizer constructor.

I tried to find documentation about migration from 3.1 to 3.2 (without success). I tried to update the recipe of api-platform. I tried to change priority. I tried to define decoration in services.yaml.

As soon as I delete the GroupsDenormalizer.php file, Api is available again. (but my code fails because my denormalizer has a job to do :) )

What did I miss? Do you have an idea?

Below, you can find the code of these two classes.

#[AsDecorator(decorates: 'api_platform.jsonld.normalizer.item', priority: 64)]
readonly class GroupsNormalizer implements NormalizerInterface, SerializerAwareInterface
{
    public function __construct(
        private NormalizerInterface $decorated,
        private GroupsGenerator     $generator,
    ) {
    }

    /**
     * @return array<string, bool>
     */
    public function getSupportedTypes(?string $format): array
    {
        return [
            Character::class => true,
            User::class => true,
        ];
    }

    public function normalize(mixed $object, string $format = null, array $context = []): float|int|bool|\ArrayObject|array|string|null
    {
        if (true /* $object instanceof ResourceInterface */) {
            $complement = $this->generator->generateReadingGroups($object);
            if (!key_exists('groups', $context)) {
                $context['groups'] = [];
            }
            $context['groups'] = array_merge($context['groups'], $complement);
        }

        return $this->decorated->normalize($object, $format, $context);
    }

    public function setSerializer(SerializerInterface $serializer): void
    {
        if ($this->decorated instanceof SerializerAwareInterface) {
            $this->decorated->setSerializer($serializer);
        }
    }

    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
    {
        return $this->decorated->supportsNormalization($data, $format);
    }
}
#[AsDecorator(decorates: 'api_platform.serializer.normalizer.item', priority: 64)]
readonly class GroupsDenormalizer implements DenormalizerInterface, SerializerAwareInterface
{
    public function __construct(
        private DenormalizerInterface $decorated,
        private GroupsGenerator       $generator,
    ) {
    }

    public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
    {
        if (key_exists('operation', $context) && $context['operation'] instanceof Operation) {
            $complement = $this->generator->generateWritingGroups($context['operation'], $type);
            if (!key_exists('groups', $context)) {
                $context['groups'] = [];
            }
            $context['groups'] = array_merge($context['groups'], $complement);
        }

        return $this->decorated->denormalize($data, $type, $format, $context);
    }

    /**
     * @return array<string, bool>
     */
    public function getSupportedTypes(?string $format): array
    {
        return [
            Character::class => true,
            User::class => true,
        ];
    }

    public function setSerializer(SerializerInterface $serializer): void
    {
        if ($this->decorated instanceof SerializerAwareInterface) {
            $this->decorated->setSerializer($serializer);
        }
    }

    public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
    {
        return $this->decorated->supportsDenormalization($data, $type, $format, $context);
    }
}

Solution

  • My getSupportedTypes wasn't excluding correctly a lot of classes.

        /**
         * @return array<string, bool>
         */
        public function getSupportedTypes(?string $format): array
        {
            return [
                Character::class => false,
                User::class => false,
                '*' => null,
            ];
        }