Symfony serializer - set circular reference global Symfony serializer - set circular reference global symfony symfony

Symfony serializer - set circular reference global


For a week have I been reading Symfony source and trying some tricks to get it work (on my project and without installing a third party bundle: not for that functionality) and I finally got one. I used CompilerPass (https://symfony.com/doc/current/service_container/compiler_passes.html)... Which works in three steps:

1. Define build method in bundle

I choosed AppBundle because it is my first bundle to load in app/AppKernel.php.

src/AppBundle/AppBundle.php

<?phpnamespace AppBundle;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\HttpKernel\Bundle\Bundle;class AppBundle extends Bundle{    public function build(ContainerBuilder $container)    {        parent::build($container);        $container->addCompilerPass(new AppCompilerPass());    }}

2. Write your custom CompilerPass

Symfony serializers are all under the serializer service. So I just fetched it and added to it a configurator option, in order to catch its instanciation.

src/AppBundle/AppCompilerPass.php

<?phpnamespace AppBundle;use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Reference;class AppCompilerPass implements CompilerPassInterface{    public function process(ContainerBuilder $container)    {        $container            ->getDefinition('serializer')            ->setConfigurator([                new Reference(AppConfigurer::class), 'configureNormalizer'            ]);    }}

3. Write your configurer...

Here, you create a class following what you wrote in the custom CompilerPass (I choosed AppConfigurer)... A class with an instance method named after what you choosed in the custom compiler pass (I choosed configureNormalizer).

This method will be called when the symfony internal serializer will be created.

The symfony serializer contains normalizers and decoders and such things as private/protected properties. That is why I used PHP's \Closure::bind method to scope the symfony serializer as $this into my lambda-like function (PHP Closure).

Then a loop through the nomalizers ($this->normalizers) help customize their behaviours. Actually, not all of those nomalizers need circular reference handlers (like DateTimeNormalizer): the reason of the condition there.

src/AppBundle/AppConfigurer.php

<?phpnamespace AppBundle;class AppConfigurer{    public function configureNormalizer($normalizer)    {        \Closure::bind(function () use (&$normalizer)        {            foreach ($this->normalizers as $normalizer)                if (method_exists($normalizer, 'setCircularReferenceHandler'))                    $normalizer->setCircularReferenceHandler(function ($object)                    {                        return $object->getId();                    });        }, $normalizer, $normalizer)();    }}

Conclusion

As said earlier, I did it for my project since I dind't wanted FOSRestBundle nor any third party bundle as I've seen over Internet as a solution: not for that part (may be for security). My controllers now stand as...

<?phpnamespace StoreBundle\Controller;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;use Symfony\Bundle\FrameworkBundle\Controller\Controller;class ProductController extends Controller{    /**     *     * @Route("/products")     *     */    public function indexAction()    {        $em = $this->getDoctrine()->getManager();        $data = $em->getRepository('StoreBundle:Product')->findAll();        return $this->json(['data' => $data]);    }    /**     *     * @Route("/product")     * @Method("POST")     *     */    public function newAction()    {        throw new \Exception('Method not yet implemented');    }    /**     *     * @Route("/product/{id}")     *     */    public function showAction($id)    {        $em = $this->getDoctrine()->getManager();        $data = $em->getRepository('StoreBundle:Product')->findById($id);        return $this->json(['data' => $data]);    }    /**     *     * @Route("/product/{id}/update")     * @Method("PUT")     *     */    public function updateAction($id)    {        throw new \Exception('Method not yet implemented');    }    /**     *     * @Route("/product/{id}/delete")     * @Method("DELETE")     *     */    public function deleteAction($id)    {        throw new \Exception('Method not yet implemented');    }}


The only way I've found is to create your own object normalizer to add the circular reference handler.

A minimal working one can be:

<?phpnamespace AppBundle\Serializer\Normalizer;use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;use Symfony\Component\PropertyAccess\PropertyAccessorInterface;use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;use Symfony\Component\Serializer\NameConverter\NameConverterInterface;class AppObjectNormalizer extends ObjectNormalizer{    public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)    {        parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);        $this->setCircularReferenceHandler(function ($object) {            return $object->getName();        });    }}

Then declare as a service with a slithly higher priority than the default one (which is -1000):

<service    id="app.serializer.normalizer.object"    class="AppBundle\Serializer\Normalizer\AppObjectNormalizer"    public="false"    parent="serializer.normalizer.object">    <tag name="serializer.normalizer" priority="-500" /></service>

This normalizer will be used by default everywhere in your project.