Symfony 序列化程序 - 设置循环引用全局

Symfony serializer - set circular reference global

有没有办法在 Symfony 的序列化器组件(不是 JMSSerializer)中使用任何配置或类似的东西设置循环引用限制?

我有一个带有 FOSRestBundle 的 REST 应用程序和一些包含其他实体的实体,这些实体也应该被序列化。但是我 运行 陷入了循环引用错误。

我知道怎么设置了:

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

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

但这必须在多个控制器中完成(对我来说是开销)。 我想在配置(.yml)中全局设置它,例如像这样:

framework: 
    serializer:
        enabled: true
        circular_limit: 5

没有找到序列化器 API 参考所以我想知道这是否可能?

我发现的唯一方法是创建您自己的对象规范化器以添加循环引用处理程序。

一个最小的工作可以是:

<?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>

默认情况下,您的项目中的所有地方都会使用此标准化器。

一个星期以来,我一直在阅读 Symfony 源代码并尝试一些技巧让它工作(在我的项目中并且没有安装第三方包:不是为了那个功能),我终于得到了一个。我使用了 CompilerPass (https://symfony.com/doc/current/service_container/compiler_passes.html)... 它分三步工作:

1。在 bundle

中定义 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。编写您的配置程序...

在这里,您创建一个 class 按照您在自定义 CompilerPass 中编写的内容(我选择 AppConfigurer)...一个 class 带有以您选择的内容命名的实例方法在自定义编译器传递中(我选择 configureNormalizer)。

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

symfony 序列化器包含规范化器和解码器以及 作为 private/protected 属性 的东西。这就是为什么我使用 PHP 的 \Closure::bind 方法将 symfony 序列化器的范围限定为 $this 到我的类似 lambda 的函数(PHP 闭包)。

然后循环遍历规范化器 ($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 也不想要任何第三方包,正如我在 Internet 上看到的那样:不是为了那部分(可能是为了安全)。我的控制器现在代表...

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

}