JMS 序列化程序 - 交叉引用问题

JMS Serializer - Cross reference issue

我的实体有两个自引用 OneToMany 关系 childrenrevisions.

<?php

namespace App\Entity\CMS;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;

/**
 * @ORM\Entity
 * @JMS\ExclusionPolicy("ALL")
 */
class Page
{
    /**
     * @var int
     *
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var Page[]|ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="CoreBundle\Entity\CMS\Page", mappedBy="parent")
     *
     * @JMS\Groups({"page_children"})
     * @JMS\Expose()
     */
    protected $children;

    /**
     * @var Page[]|ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="CoreBundle\Entity\CMS\Page", mappedBy="page")
     *
     * @JMS\Groups({"revisions"})
     * @JMS\Expose()
     *
     */
    protected $revisions;

    /**
     * @var string
     *
     * @ORM\Column(type="string", value={"main", "revision"})
     *
     * @JMS\Expose()
     *
     */
    protected $type;

    #...
}

我公开了两个集合 - childrenrevisions。另外 type 被暴露 - 它是 Page 是否属于 revisions 的指标。

请求 {{host}}/api/pages?expand=page_children returns 结果,其中包括 Pages 两种类型。

{
    "id": "1",
    "type": "main",
    "children": [
        {
            "id": "3",
            "type": "main",
            "children": [
                {
                    "id": "5",
                    "type": "main",
                    "children": []

                },
                {
                    "id": "6",
                    "type": "revision",
                    "children": []

                }
            ],
        },
        {
            "id": "4,
            "type": "revision",
            "children": []
        }
    ],
    "id": "2',
    "type": "revision",
    "children": []
}

我想从响应 Pages 中排除 revision 类型。所以我的最终结果是这样的:

{
    "id": "1",
    "type": "main",
    "children": [
        {
            "id": "3",
            "type": "main",
            "children": [
                {
                    "id": "5",
                    "type": "main",
                    "children": []
                }
            ]
        }
    ]
}

通常,我使用 LexikFormFilterBundle 来过滤结果。 但是在这种情况下,组合请求如下:

{{host}}/api/expand=page_children&page_filter[type]=main

仅适用于第一级结果。

我考虑过Dynamic Exclusion Strategy or Subscribing Handler。不幸的是,我无法找出解决方案。

您可以通过 class 中的自定义方法并告诉 JMS 使用它。

/** * @VirtualProperty * @SerializedName("my_collection") */ public function getMyCollection() { return $this->collection->filterByType..... }

如果您想使其动态化,您可以在虚拟 属性 中存储一些数据并在此方法中使用它。 要控制深度,请使用 @JMS\MaxDepth(depth=1).

但从概念上讲,这似乎是一个非常糟糕的主意。我宁愿审查范围,也可能以适当的方式拆分。

我通过实施 Subscribing Handler

解决了这个问题
<?php

namespace CoreBundle\Serializer\Subscriber\CMS;

use App\Entity\CMS\Page;
use JMS\Serializer\EventDispatcher\Events;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use JMS\Serializer\Handler\SubscribingHandlerInterface;

class PageSubscriber extends SubscribingHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            [
                'event' => Events::PRE_SERIALIZE,
                'method' => 'onPreSerialize',
                'class' => Page::class,
                'format' => 'json',
            ],
        ];
    }

    /**
     * @param PreSerializeEvent $event
     */
    public function onPreSerialize(PreSerializeEvent $event)
    {
        $entity = $event->getObject();

        if (!$entity instanceof Page) {
            return;
        }
        if ($this->isSerialisingForGroup($event, 'exclude_revisions')) {
            $this->excludeRevisions($entity);
        }
    }

    /**
     * @param Page $page
     */
    private function excludeRevisions(Page $page): void
    {
        foreach ($page->getChildren() as $child) {
            if ($child->getStatus() === 'revision') {
                $page->removeChild($child);
            }
        }
    }
}

缺点:此时所有数据都被抓取。元素 "type": "revision" 将包含在非第一级中,它会展开,这可能导致 Allowed memory size exhausted.

另一种可能的方法是使用 Doctrine postLoad 事件。

<?php

namespace App\Service\Doctrine;

use App\Entity\CMS\Page;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use RuntimeException;

class PageListener implements EventSubscriber
{
    /** @var bool $canFlush */
    private $canFlush = true;

    /**
     * {@inheritdoc}
     */
    public function getSubscribedEvents()
    {
        return [
            Events::postLoad,
            Events::onFlush,
        ];
    }

    /**
     * @param Page $page
     */
    public function onPostLoad(Page $page): void
    {
        $children = $page->getChildren();

        foreach ($children as $child) {
            if ($child->getStatus === 'revision') {
                $page->removeChild($child);
            }
        }
    }

    /**
     * @param OnFlushEventArgs $eventArgs
     */
    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityDeletions() as $entityDeletion) {
            if ($entityDeletion instanceof Page){
                throw new RuntimeException('Flushing Page at this point will remove all Revisions.');
            }
        }
    }
}

缺点:可能很危险,并且会缩小未来可能的变化范围。