Сериализатор Symfony - установить глобальную циклическую ссылку

Есть ли способ установить лимит круговой ссылки в компоненте сериализатора Symfony (не JMSSerializer) с какой-либо конфигурацией или чем-то в этом роде?

У меня есть приложение REST с FOSRestBundle и некоторыми объектами, которые содержат другие объекты, которые также должны быть сериализованы. Но я сталкиваюсь с ошибками циклических ссылок.

Я знаю, как установить это так:

$encoder    = new JsonEncoder();
$normalizer = new ObjectNormalizer();

$normalizer->setCircularReferenceHandler(function ($object) {
     return $object->getName();
});

Но это должно быть сделано более чем в одном контроллере (накладные расходы для меня). Я хочу установить его глобально в конфигурации (.yml), например. как это:

framework: 
    serializer:
        enabled: true
        circular_limit: 5

Не нашел ссылки на API сериализатора для этого, поэтому мне интересно, возможно ли это или нет?


person kinske    schedule 12.02.2016    source источник


Ответы (2)


Единственный способ, который я нашел, - это создать свой собственный нормализатор объектов, чтобы добавить обработчик циклических ссылок.

Минимальный рабочий может быть:

<?php

namespace 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();
        });
    }
}

Затем объявите как службу с чуть более высоким приоритетом, чем по умолчанию (который равен -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>

Этот нормализатор будет использоваться по умолчанию везде в вашем проекте.

person magnetik    schedule 03.08.2016
comment
Метод \Symfony\Component\Serializer\Normalizer\AbstractNormalizer::setCircularReferenceHandler устарел, начиная с Symfony 4.2. Вы должны использовать ключ circular_reference_handler контекста: $this->defaultContext['circular_reference_handler'] = function ($object) { return $object->getName(); }; - person Konpaka; 09.02.2019
comment
@Konpaka, именно то, что должно быть возможно в файле конфигурации. - person rishta; 17.10.2019

В течение недели я читал исходный код Symfony и пробовал некоторые приемы, чтобы заставить его работать (в моем проекте и без установки стороннего пакета: не для этой функциональности), и наконец я получил его. Я использовал CompilerPass (https://symfony.com/doc/current/service_container/compiler_passes.html)... который работает в три этапа:

1. Определите метод build в пакете

Я выбрал AppBundle, потому что это мой первый пакет, загружаемый в app/AppKernel.php.

src/AppBundle/AppBundle.php

<?php

namespace 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. Напишите свой собственный CompilerPass

Все сериализаторы Symfony находятся в службе serializer. Поэтому я просто взял его и добавил к нему параметр configurator, чтобы поймать его экземпляр.

src/AppBundle/AppCompilerPass.php

<?php

namespace 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. Напишите свой конфигуратор...

Здесь вы создаете класс в соответствии с тем, что вы написали в пользовательском CompilerPass (я выбрал AppConfigurer)... Класс с методом экземпляра, названным в честь того, что вы выбрали в пользовательском проходе компилятора (я выбрал configureNormalizer).

Этот метод будет вызываться при создании внутреннего сериализатора symfony.

Сериализатор symfony содержит нормализаторы и декодеры и такие вещи, как частные/защищенные свойства. Вот почему я использовал PHP-метод \Closure::bind, чтобы ограничить сериализатор symfony как $this в моей лямбда-подобной функции (PHP Closure).

Затем цикл по нормализаторам ($this->normalizers) помогает настроить их поведение. На самом деле, не всем этим нормализаторам нужны циклические обработчики ссылок (такие как DateTimeNormalizer): причина условия там.

src/AppBundle/AppConfigurer.php

<?php

namespace 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)();
    }
}

Вывод

Как было сказано ранее, я сделал это для своего проекта, так как мне не нужен FOSRestBundle или какой-либо сторонний пакет, который я видел в Интернете в качестве решения: не для этой части (может быть, для безопасности). Мои контроллеры теперь стоят как...

<?php

namespace 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');
    }

}
person Salathiel Genèse    schedule 07.10.2017
comment
Отличное решение! Это та самая причина, по которой компилятор прошел там, где создавался. - person Matías Navarro Carter; 22.01.2018
comment
Слишком много возможностей, слишком мало времени, слишком мало краткой и хорошей и слишком много слишком широкой документации. И в результате всего этого, как упомянул OP, он провел неделю, пока не нашел это идеальное решение. Это роскошь, которой нет у многих из нас. :( Поэтому лично я бы выбрал это решение: stackoverflow.com/a/44286659/261332. - person userfuser; 13.02.2018
comment
@urserfuser Я прочитал это решение вместе с вашим комментарием. (1) Не у всех есть роскошь времени, поэтому я разместил найденное решение (2) Что касается идеального решения, я скажу, что это не так, но я не хотел создавать сериализатор (JSON, XML, YAML и т. д.) для каждого маршрута моего API. (3) Что касается дизайна ООП, я не горжусь этим \Closure::bind. Поскольку это ядро ​​​​php, я вижу очень похожее на утечку дизайна PHP. (4) И, наконец, я все равно предпочту это решение вашему выбору, потому что Symfony будет автоматически обрабатывать форматы JSON, YAML или XML. - person Salathiel Genèse; 14.02.2018