Completed
Push — master ( 9a6c84...e77310 )
by Alan
04:44
created

PublishMercureUpdatesListener::onFlush()   B

Complexity

Conditions 9
Paths 129

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 15
nc 129
nop 1
dl 0
loc 23
rs 7.8138
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Bridge\Doctrine\EventListener;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
18
use ApiPlatform\Core\Api\UrlGeneratorInterface;
19
use ApiPlatform\Core\Bridge\Symfony\Messenger\DispatchTrait;
20
use ApiPlatform\Core\Exception\InvalidArgumentException;
21
use ApiPlatform\Core\Exception\RuntimeException;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
24
use Doctrine\Common\EventArgs;
25
use Doctrine\ODM\MongoDB\Event\OnFlushEventArgs as MongoDbOdmOnFlushEventArgs;
26
use Doctrine\ORM\Event\OnFlushEventArgs as OrmOnFlushEventArgs;
27
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
28
use Symfony\Component\Mercure\Update;
29
use Symfony\Component\Messenger\MessageBusInterface;
30
use Symfony\Component\Serializer\SerializerInterface;
31
32
/**
33
 * Publishes resources updates to the Mercure hub.
34
 *
35
 * @author Kévin Dunglas <[email protected]>
36
 *
37
 * @experimental
38
 */
39
final class PublishMercureUpdatesListener
40
{
41
    use DispatchTrait;
42
    use ResourceClassInfoTrait;
43
44
    private $iriConverter;
45
    private $serializer;
46
    private $publisher;
47
    private $expressionLanguage;
48
    private $createdObjects;
49
    private $updatedObjects;
50
    private $deletedObjects;
51
    private $formats;
52
53
    /**
54
     * @param array<string, string[]|string> $formats
55
     */
56
    public function __construct(ResourceClassResolverInterface $resourceClassResolver, IriConverterInterface $iriConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerInterface $serializer, array $formats, MessageBusInterface $messageBus = null, callable $publisher = null, ExpressionLanguage $expressionLanguage = null)
57
    {
58
        if (null === $messageBus && null === $publisher) {
59
            throw new InvalidArgumentException('A message bus or a publisher must be provided.');
60
        }
61
62
        $this->resourceClassResolver = $resourceClassResolver;
63
        $this->iriConverter = $iriConverter;
64
        $this->resourceMetadataFactory = $resourceMetadataFactory;
65
        $this->serializer = $serializer;
66
        $this->formats = $formats;
67
        $this->messageBus = $messageBus;
68
        $this->publisher = $publisher;
69
        $this->expressionLanguage = $expressionLanguage ?? class_exists(ExpressionLanguage::class) ? new ExpressionLanguage() : null;
70
        $this->reset();
71
    }
72
73
    /**
74
     * Collects created, updated and deleted objects.
75
     */
76
    public function onFlush(EventArgs $eventArgs): void
77
    {
78
        if ($eventArgs instanceof OrmOnFlushEventArgs) {
79
            $uow = $eventArgs->getEntityManager()->getUnitOfWork();
80
        } elseif ($eventArgs instanceof MongoDbOdmOnFlushEventArgs) {
81
            $uow = $eventArgs->getDocumentManager()->getUnitOfWork();
82
        } else {
83
            return;
84
        }
85
86
        $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityInsertions' : 'getScheduledDocumentInsertions';
87
        foreach ($uow->{$methodName}() as $object) {
88
            $this->storeObjectToPublish($object, 'createdObjects');
89
        }
90
91
        $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityUpdates' : 'getScheduledDocumentUpdates';
92
        foreach ($uow->{$methodName}() as $object) {
93
            $this->storeObjectToPublish($object, 'updatedObjects');
94
        }
95
96
        $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityDeletions' : 'getScheduledDocumentDeletions';
97
        foreach ($uow->{$methodName}() as $object) {
98
            $this->storeObjectToPublish($object, 'deletedObjects');
99
        }
100
    }
101
102
    /**
103
     * Publishes updates for changes collected on flush, and resets the store.
104
     */
105
    public function postFlush(): void
106
    {
107
        try {
108
            foreach ($this->createdObjects as $object) {
109
                $this->publishUpdate($object, $this->createdObjects[$object]);
110
            }
111
112
            foreach ($this->updatedObjects as $object) {
113
                $this->publishUpdate($object, $this->updatedObjects[$object]);
114
            }
115
116
            foreach ($this->deletedObjects as $object) {
117
                $this->publishUpdate($object, $this->deletedObjects[$object]);
118
            }
119
        } finally {
120
            $this->reset();
121
        }
122
    }
123
124
    private function reset(): void
125
    {
126
        $this->createdObjects = new \SplObjectStorage();
127
        $this->updatedObjects = new \SplObjectStorage();
128
        $this->deletedObjects = new \SplObjectStorage();
129
    }
130
131
    /**
132
     * @param object $object
133
     */
134
    private function storeObjectToPublish($object, string $property): void
135
    {
136
        if (null === $resourceClass = $this->getResourceClass($object)) {
137
            return;
138
        }
139
140
        $value = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
0 ignored issues
show
Bug introduced by
The method create() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

140
        $value = $this->resourceMetadataFactory->/** @scrutinizer ignore-call */ create($resourceClass)->getAttribute('mercure', false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
141
        if (false === $value) {
142
            return;
143
        }
144
145
        if (\is_string($value)) {
146
            if (null === $this->expressionLanguage) {
147
                throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".');
148
            }
149
150
            $value = $this->expressionLanguage->evaluate($value, ['object' => $object]);
151
        }
152
153
        if (true === $value) {
154
            $value = [];
155
        }
156
157
        if (!\is_array($value)) {
158
            throw new InvalidArgumentException(sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of targets or a valid expression, "%s" given.', $resourceClass, \gettype($value)));
159
        }
160
161
        if ('deletedObjects' === $property) {
162
            $this->deletedObjects[(object) [
163
                'id' => $this->iriConverter->getIriFromItem($object),
164
                'iri' => $this->iriConverter->getIriFromItem($object, UrlGeneratorInterface::ABS_URL),
165
            ]] = $value;
166
167
            return;
168
        }
169
170
        $this->{$property}[$object] = $value;
171
    }
172
173
    /**
174
     * @param object $object
175
     */
176
    private function publishUpdate($object, array $targets): void
177
    {
178
        if ($object instanceof \stdClass) {
179
            // By convention, if the object has been deleted, we send only its IRI.
180
            // This may change in the feature, because it's not JSON Merge Patch compliant,
181
            // and I'm not a fond of this approach.
182
            $iri = $object->iri;
183
            /** @var string $data */
184
            $data = json_encode(['@id' => $object->id]);
185
        } else {
186
            $resourceClass = $this->getObjectClass($object);
187
            $context = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('normalization_context', []);
188
189
            $iri = $this->iriConverter->getIriFromItem($object, UrlGeneratorInterface::ABS_URL);
190
            $data = $this->serializer->serialize($object, key($this->formats), $context);
191
        }
192
193
        $update = new Update($iri, $data, $targets);
194
        $this->messageBus ? $this->dispatch($update) : ($this->publisher)($update);
195
    }
196
}
197