Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JMS Serializer - Cross reference issue

My entity has two self-referencing OneToMany relationships children and revisions.

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

    #...
}

I am exposing two collections - children and revisions. Additionally type is exposed - it's an indicator if Page belongs to revisions or not.

Request {{host}}/api/pages?expand=page_children returns result which includes Pages of both types.

{
    "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": []
}

I'd like to exclude from the response Pages which type is revision. So my final result would look like:

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

Usually, to filter results I'm using LexikFormFilterBundle. However in this case combined request like:

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

works only for the first level results.

I thought about Dynamic Exclusion Strategy or Subscribing Handler. Unfortunately, I cannot figure out the solution.

like image 765
Łukasz D. Tulikowski Avatar asked Feb 23 '26 18:02

Łukasz D. Tulikowski


1 Answers

I solved this problem implementing 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);
            }
        }
    }
}

Disadvantage: All data is fetched at this point. Elements "type": "revision" will be included in non-first level and it will expand which can lead to Allowed memory size exhausted.

Another possible approach would be to use Doctrine postLoad Event.

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

Disadvantage: Can be dangerous and narrow possible future changes.

like image 152
Łukasz D. Tulikowski Avatar answered Feb 27 '26 01:02

Łukasz D. Tulikowski



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!