1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\EventDispatcher\Subscriber;
6
7use Doctrine\Common\Persistence\Proxy;
8use Doctrine\ODM\MongoDB\PersistentCollection as MongoDBPersistentCollection;
9use Doctrine\ODM\PHPCR\PersistentCollection as PHPCRPersistentCollection;
10use Doctrine\ORM\PersistentCollection;
11use Doctrine\ORM\Proxy\Proxy as ORMProxy;
12use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
13use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
14use JMS\Serializer\EventDispatcher\PreSerializeEvent;
15
16final class DoctrineProxySubscriber implements EventSubscriberInterface
17{
18    /**
19     * @var bool
20     */
21    private $skipVirtualTypeInit = true;
22
23    /**
24     * @var bool
25     */
26    private $initializeExcluded = false;
27
28    public function __construct(bool $skipVirtualTypeInit = true, bool $initializeExcluded = false)
29    {
30        $this->skipVirtualTypeInit = (bool) $skipVirtualTypeInit;
31        $this->initializeExcluded = (bool) $initializeExcluded;
32    }
33
34    public function onPreSerialize(PreSerializeEvent $event): void
35    {
36        $object = $event->getObject();
37        $type = $event->getType();
38
39        // If the set type name is not an actual class, but a faked type for which a custom handler exists, we do not
40        // modify it with this subscriber. Also, we forgo autoloading here as an instance of this type is already created,
41        // so it must be loaded if its a real class.
42        $virtualType = !class_exists($type['name'], false);
43
44        if ($object instanceof PersistentCollection
45            || $object instanceof MongoDBPersistentCollection
46            || $object instanceof PHPCRPersistentCollection
47        ) {
48            if (!$virtualType) {
49                $event->setType('ArrayCollection');
50            }
51
52            return;
53        }
54
55        if (($this->skipVirtualTypeInit && $virtualType) ||
56            (!$object instanceof Proxy && !$object instanceof ORMProxy)
57        ) {
58            return;
59        }
60
61        // do not initialize the proxy if is going to be excluded by-class by some exclusion strategy
62        if (false === $this->initializeExcluded && !$virtualType) {
63            $context = $event->getContext();
64            $exclusionStrategy = $context->getExclusionStrategy();
65            $metadata = $context->getMetadataFactory()->getMetadataForClass(get_parent_class($object));
66            if (null !== $metadata && null !== $exclusionStrategy && $exclusionStrategy->shouldSkipClass($metadata, $context)) {
67                return;
68            }
69        }
70
71        $object->__load();
72
73        if (!$virtualType) {
74            $event->setType(get_parent_class($object), $type['params']);
75        }
76    }
77
78    public function onPreSerializeTypedProxy(PreSerializeEvent $event, string $eventName, string $class, string $format, EventDispatcherInterface $dispatcher): void
79    {
80        $type = $event->getType();
81        // is a virtual type? then there is no need to change the event name
82        if (!class_exists($type['name'], false)) {
83            return;
84        }
85
86        $object = $event->getObject();
87        if ($object instanceof Proxy) {
88            $parentClassName = get_parent_class($object);
89
90            // check if this is already a re-dispatch
91            if (strtolower($class) !== strtolower($parentClassName)) {
92                $event->stopPropagation();
93                $newEvent = new PreSerializeEvent($event->getContext(), $object, ['name' => $parentClassName, 'params' => $type['params']]);
94                $dispatcher->dispatch($eventName, $parentClassName, $format, $newEvent);
95
96                // update the type in case some listener changed it
97                $newType = $newEvent->getType();
98                $event->setType($newType['name'], $newType['params']);
99            }
100        }
101    }
102
103    /**
104     * {@inheritdoc}
105     */
106    public static function getSubscribedEvents()
107    {
108        return [
109            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeTypedProxy', 'interface' => Proxy::class],
110            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PersistentCollection::class],
111            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => MongoDBPersistentCollection::class],
112            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PHPCRPersistentCollection::class],
113            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => Proxy::class],
114        ];
115    }
116}
117