maker-bundle: [Serializer] Custom Normalizer broken after upgrading to 6.1

Symfony version(s) affected

6.1

Description

My custom Normalizer is broken since i’ve upgraded to Symfony 6.1. This problem seems to be related to the new Serializer Profiler.

Maybe I forgot something?

Thanks, Alex

How to reproduce

Just use this example :

https://symfony.com/doc/6.1/serializer/custom_normalizer.html

Possible Solution

No response

Additional Context

FileNormalizer::__construct(): Argument symfony/symfony#2 ($normalizer) must be of type Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer, Symfony\\Component\\Serializer\\Debug\\TraceableNormalizer given

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 18
  • Comments: 60 (25 by maintainers)

Commits related to this issue

Most upvoted comments

It hasn’t been fixed in the sense that there is no fix to be made in the code imho. The docs need to be updated to explain about this issue and how to properly wire up your custom normalizer.

Here is an example on how I do it:

<?php

namespace App\Serializer\Normalizer;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

readonly class MyCustomNormalizer implements DenormalizerInterface
{
    private NormalizerInterface&DenormalizerInterface $normalizer;

    public function __construct(
        #[Autowire(service: 'serializer.normalizer.object')] NormalizerInterface&DenormalizerInterface $normalizer,
    ) {
        $this->normalizer = $normalizer;
    }
}

Obviously you can replace the serializer.normalizer.object service with any normalizer you like.

Now this example is using Symfony 6.3 with PHP 8.2, but the same principles apply for older versions although you’d need to make some modifications like replace the Autowire attribute etc.

A possible workaround, that would work with the new TraceableNormalizer, is to use the new Autowire attribute:

class CustomNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
    public function __construct(
        #[Autowire(service: ObjectNormalizer::class)]
        private NormalizerInterface $normalizer
    ) {
    }
}

But I agree with @derrabus that it is somewhat crazy, that symfony injects an object with an incompatiable type.

I’m not to familiar with how symfonys dependency injection works internally, but is the “symfony/proxy-manager-bridge” package not able to solve this problem? A “lazy” service will be replaced by a proxy service, so wouldn’t it be possible to create a proxy object that has the TraceableNormalizer functionality? Currently the service definition gets replaced by a decorator.

The library the bridge is based on, Ocarmius/ProxyManager, has the Access Interceptor Value Holder Proxy concept. This should be able to solve the problem. Bascially I’m suggesting that the service is not replaced by “TraceableNormalizer” (this applies for all traceable decorators, like TraceableAuthenticator), but by a proxy object.

NormalizerInterface concrete implementations are not meant to be referenced by something else than Serializer itself.

Not sure about that. In the documented case, the developer does not just want any normalizer implementation. They explicitly want the behavior of ObjectNormalizer because it solves 95% of their problem already. The alternatives are either to extend ObjectNormalizer or to work with NormalizerAwareInterface and work with priorities and flags that are pushed to the context.

The former is a technique which we should discourage imho. And the latter feels a bit too complicated for what should be achieved here.

I think that the example from the docs is correct and should continue to work after an upgrade. We should treat this as a bug.

The serializer topic aside: If I autowire a constructor parameter with a Foo type, I find it astonishing that I receive a service that is not a Foo.

this “possible workaround” does not work for me 😕 (Circular reference detected)

Well, I’m not sure the normalizer services were actually meant to be autowirable based on their class name. Supporting that means that we don’t support decorating them (and so that we cannot apply the traceable decorators on them).

To me, the autowirable names in core should only about interfaces for that reason.

I think this feature should be reverted. Nested normalizers are an overused method. Or the old and new should continue to work at the same time. I will continue with 6.0 for a while.

I am having the same issue. Let me know if you need any help reproducing this one.

It happens when a Custom Normalizer is autowired with the ObjectNormalizer service.

I found a solution.

  1. Inject object normalizer by myself
App\Serializer\Normalizer\OrderNormalizer:
    calls:
        -   setBaseNormalizer: ['@serializer.normalizer.object']
            
    public function setBaseNormalizer($normalizer)
    {
        $this->normalizer = $normalizer;
    }

Actually, with a fresh Symfony instance, I managed to make it work.

You can leverage the NormalizerAwareInterface like the following:

final class FoobarNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;
}
services:
    App\Serializer\Normalizer\FoobarNormalizer:
        calls:
            - setNormalizer: ['@serializer.normalizer.object']

Or using a constructor injection like the following:

final class FoobarNormalizer implements NormalizerInterface
{
    public function __construct(
        private NormalizerInterface $normalizer,
    ) {
    }
}
services:
    App\Serializer\Normalizer\FoobarNormalizer:
        arguments:
            $normalizer: '@serializer.normalizer.object'

In either way, you must never use a concrete implementation of any normalizer/encoder in the code, you should inject the interface and configure your container to inject the proper instance. Therefore, IMHO the maker bundle should be updated accordingly.

And if you don’t want to specify which normalizer instance to use because you don’t care, be aware that the constructor injection won’t work because of a circular reference exception (with and without the debug), therefore my advice is to always use the NormalizerAwareInterface.

If I add array $context, my supports method will be incompatible with interface

Nope, it won’t: https://3v4l.org/sKOnn

Ok the only way I found to inject denormalizer into my own denormalizer is to disable autowiring and inject @serializer.normalizer.object directly.

That’s my personal preference.

Ok the only way I found to inject denormalizer into my own denormalizer is to disable autowiring and inject @serializer.normalizer.object directly.

See symfony/symfony#46625

Sorry if this is a dumb question, but can we not fix this in the DIC, making sure that if a decorated service is selected to be autowired, but the decorator does not satisfy the type hint, it would skip decoration and inject the decorated service?

Possibly this could be done optionally so that decorators could be marked optional (as in this case, it’s a “nice to have” feature, a user defined decorator I would assume should error if it cannot be autowired as such).

Actually, my perspective is that we must rely on abstraction here, and it looks weird to me to inject a concrete normalizer implementation. If anyone wants to inject the specific ObjectNormalizer, he might inject a NormalizerInterface and let the dependency injection bind the proper implementation.

That being said, it’s a BC break, therefore so far I can see three ways to go:

  • Keep it like that and accept the BC break because we assume that that use case might be related to a weird implementation.
  • Reducing the feature by only decorating the Serializer, this will imply that we won’t be able to retrieve anymore any nested normalizer, only the first selected normalizer will be traced.
  • Sadly, reverting (ie: removing) that feature completely.

the developer does not just want any normalizer implementation. They explicitly want the behavior of ObjectNormalizer because it solves 95% of their problem already.

My point is that this should be done via an explicit service definition so the code only knows about NormalizerInterface, and the config tells which implementation should actually be injected. Targeting ObjectNormalizer directly as a constructor arg should be avoided from a design POV. Same for the “decoration” use case, proper decoration is made against an abstraction, not a concrete implementation.

But for Example we decorate the pagination Normalizer of Api Platform… that have some abstract methods like getPaginationData… and then in local dev I have a crash saying that TraceableNormalizer don’t have any getPaginationData method 😭

That is a good point. This could be easily fixed by adding php’s magic __call method to the TraceableNormalizer implementation. As far as I can see, exactly this was already implemented for TreaceableAuthenticator, TraceableEventDispatcher, LoggingTranslator etc…

I can submit a PR incase this function wasn’t omitted on purpose?

EDIT: still have the issue in local dev with profiler enabled in fact…

Do you import that Autowire class, I had it not imported and this was leading to “Circular reference detected” error. After import seems tat it works in dev env

Similar (?) issue. On 6.0, the following service declaration works: services.yaml

  Infrastructure\Shared\ApiPlatform\Serializer\JsonLdNormalizer:
    decorates: 'api_platform.jsonld.normalizer.item'
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class JsonLdNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
    public function __construct(
        private readonly IriConverterInterface $iriConverter,
        private readonly ItemNormalizer $decorated,
    ) {
    }

    ......
}

On 6.1 I suddenly get the error:

Cannot autowire service "Infrastructure\Shared\ApiPlatform\Serializer\JsonLdNormalizer": argument "$decorated" of method "__construct()" references class "ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer" but no such   
  service exists. Try changing the type-hint to one of its parents: interface "Symfony\Component\Serializer\Normalizer\DenormalizerInterface", or interface "Symfony\Component\Serializer\Normalizer\NormalizerInterface". 

If I change my service declaration:

  Infrastructure\Shared\ApiPlatform\Serializer\JsonLdNormalizer:
    decorates: 'api_platform.jsonld.normalizer.item'
    arguments:
      $decorated: '@.inner'

The error message changes:

Infrastructure\Shared\ApiPlatform\Serializer\JsonLdNormalizer::__construct(): Argument symfony/symfony#2 ($decorated) must be of type ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer,  
     Symfony\Component\Serializer\Debug\TraceableNormalizer given