Passed
Pull Request — master (#2282)
by Kévin
04:04
created

PublishMercureUpdatesListener   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 24
eloc 70
dl 0
loc 141
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A reset() 0 5 1
A postFlush() 0 16 4
A onFlush() 0 14 4
A __construct() 0 14 4
A publishUpdate() 0 15 3
B storeEntityToPublish() 0 38 8
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\Exception\InvalidArgumentException;
20
use ApiPlatform\Core\Exception\RuntimeException;
21
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22
use ApiPlatform\Core\Util\ClassInfoTrait;
23
use Doctrine\ORM\Event\OnFlushEventArgs;
24
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
25
use Symfony\Component\Mercure\Update;
26
use Symfony\Component\Messenger\MessageBusInterface;
27
use Symfony\Component\Serializer\SerializerInterface;
28
29
/**
30
 * Publishes resources updates to the Mercure hub.
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 *
34
 * @experimental
35
 */
36
final class PublishMercureUpdatesListener
37
{
38
    use ClassInfoTrait;
39
40
    private $resourceClassResolver;
41
    private $iriConverter;
42
    private $resourceMetadataFactory;
43
    private $serializer;
44
    private $messageBus;
45
    private $publisher;
46
    private $expressionLanguage;
47
    private $createdEntities;
48
    private $updatedEntities;
49
    private $deletedEntities;
50
51
    public function __construct(ResourceClassResolverInterface $resourceClassResolver, IriConverterInterface $iriConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerInterface $serializer, MessageBusInterface $messageBus = null, callable $publisher = null, ExpressionLanguage $expressionLanguage = null)
52
    {
53
        if (null === $messageBus && null === $publisher) {
54
            throw new InvalidArgumentException('A message bus or a publisher must be provided.');
55
        }
56
57
        $this->resourceClassResolver = $resourceClassResolver;
58
        $this->iriConverter = $iriConverter;
59
        $this->resourceMetadataFactory = $resourceMetadataFactory;
60
        $this->serializer = $serializer;
61
        $this->messageBus = $messageBus;
62
        $this->publisher = $publisher;
63
        $this->expressionLanguage = $expressionLanguage ?? class_exists(ExpressionLanguage::class) ? new ExpressionLanguage() : null;
64
        $this->reset();
65
    }
66
67
    /**
68
     * Collects created, updated and deleted entities.
69
     */
70
    public function onFlush(OnFlushEventArgs $eventArgs)
71
    {
72
        $uow = $eventArgs->getEntityManager()->getUnitOfWork();
73
74
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
75
            $this->storeEntityToPublish($entity, 'createdEntities');
76
        }
77
78
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
79
            $this->storeEntityToPublish($entity, 'updatedEntities');
80
        }
81
82
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
83
            $this->storeEntityToPublish($entity, 'deletedEntities');
84
        }
85
    }
86
87
    /**
88
     * Publishes updates for changes collected on flush, and resets the store.
89
     */
90
    public function postFlush()
91
    {
92
        try {
93
            foreach ($this->createdEntities as $entity) {
94
                $this->publishUpdate($entity, $this->createdEntities[$entity]);
95
            }
96
97
            foreach ($this->updatedEntities as $entity) {
98
                $this->publishUpdate($entity, $this->updatedEntities[$entity]);
99
            }
100
101
            foreach ($this->deletedEntities as $entity) {
102
                $this->publishUpdate($entity, $this->deletedEntities[$entity]);
103
            }
104
        } finally {
105
            $this->reset();
106
        }
107
    }
108
109
    private function reset(): void
110
    {
111
        $this->createdEntities = new \SplObjectStorage();
112
        $this->updatedEntities = new \SplObjectStorage();
113
        $this->deletedEntities = new \SplObjectStorage();
114
    }
115
116
    /**
117
     * @param object $entity
118
     */
119
    private function storeEntityToPublish($entity, string $property): void
120
    {
121
        $resourceClass = $this->getObjectClass($entity);
122
        if (!$this->resourceClassResolver->isResourceClass($resourceClass)) {
123
            return;
124
        }
125
126
        $value = $this->resourceMetadataFactory->create($resourceClass)->getAttribute('mercure', false);
127
        if (false === $value) {
128
            return;
129
        }
130
131
        if (\is_string($value)) {
132
            if (null === $this->expressionLanguage) {
133
                throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".');
134
            }
135
136
            $value = $this->expressionLanguage->evaluate($value, ['object' => $entity]);
137
        }
138
139
        if (true === $value) {
140
            $value = [];
141
        }
142
143
        if (!\is_array($value)) {
144
            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)));
145
        }
146
147
        if ('deletedEntities' === $property) {
148
            $this->deletedEntities[(object) [
149
                'id' => $this->iriConverter->getIriFromItem($entity),
150
                'iri' => $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL),
151
            ]] = $value;
152
153
            return;
154
        }
155
156
        $this->$property[$entity] = $value;
157
    }
158
159
    /**
160
     * @param object|string $entity
161
     */
162
    private function publishUpdate($entity, array $targets): void
163
    {
164
        if ($entity instanceof \stdClass) {
165
            // By convention, if the entity has been deleted, we send only its IRI
166
            // This may change in the feature, because it's not JSON Merge Patch compliant,
167
            // and I'm not a fond of this approach
168
            $iri = $entity->iri;
169
            $data = json_encode(['@id' => $entity->id]);
170
        } else {
171
            $iri = $this->iriConverter->getIriFromItem($entity, UrlGeneratorInterface::ABS_URL);
172
            $data = $this->serializer->serialize($entity, 'jsonld');
173
        }
174
175
        $update = new Update($iri, $data, $targets);
176
        $this->messageBus ? $this->messageBus->dispatch($update) : ($this->publisher)($update);
177
    }
178
}
179