Passed
Push — main ( a8d12f...4cb83a )
by Daniel
04:05
created

PropagateUpdatesListener::refreshUpdatedEntities()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 7
ccs 0
cts 6
cp 0
crap 20
rs 10
1
<?php
2
3
namespace Silverback\ApiComponentsBundle\EventListener\Doctrine;
4
5
use ApiPlatform\Api\IriConverterInterface;
6
use ApiPlatform\Api\ResourceClassResolverInterface;
7
use ApiPlatform\Api\UrlGeneratorInterface;
8
use ApiPlatform\Exception\InvalidArgumentException;
9
use ApiPlatform\Metadata\GetCollection;
10
use Doctrine\Common\Util\ClassUtils;
11
use Doctrine\ORM\EntityRepository;
12
use Doctrine\ORM\Event\OnFlushEventArgs;
13
use Doctrine\ORM\Event\PostFlushEventArgs;
14
use Doctrine\ORM\PersistentCollection;
15
use Doctrine\ORM\UnitOfWork;
16
use Doctrine\Persistence\ManagerRegistry;
17
use Doctrine\Persistence\ObjectManager;
18
use Doctrine\Persistence\ObjectRepository;
19
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
20
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
21
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
22
use Silverback\ApiComponentsBundle\HttpCache\ResourceChangedPropagatorInterface;
23
use Silverback\ApiComponentsBundle\Repository\Core\ComponentPositionRepository;
24
use Symfony\Component\PropertyAccess\PropertyAccess;
25
use Symfony\Component\PropertyAccess\PropertyAccessor;
26
27
class PropagateUpdatesListener
28
{
29
    private PropertyAccessor $propertyAccessor;
30
    private ObjectRepository|EntityRepository $collectionRepository;
31
    private \SplObjectStorage $updatedResources;
32
    private array $pageDataPropertiesChanged = [];
33
    private array $updatedCollectionClassToIriMapping = [];
34
35
    /**
36
     * @param IriConverterInterface $iriConverter
37
     * @param ManagerRegistry $entityManager
38
     * @param iterable|ResourceChangedPropagatorInterface[] $resourceChangedPropagators
39
     * @param ResourceClassResolverInterface $resourceClassResolver
40
     * @param PageDataProvider $pageDataProvider
41
     * @param ComponentPositionRepository $positionRepository
42
     */
43
    public function __construct(
44
        private readonly IriConverterInterface $iriConverter,
45
        ManagerRegistry $entityManager,
46
        private readonly iterable $resourceChangedPropagators,
47
        private readonly ResourceClassResolverInterface $resourceClassResolver,
48
        private readonly PageDataProvider $pageDataProvider,
49
        private readonly ComponentPositionRepository $positionRepository
50
    ) {
51
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
52
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
53
        $this->reset();
54
    }
55
56
    public function onFlush(OnFlushEventArgs $eventArgs): void
57
    {
58
        $em = $eventArgs->getObjectManager();
59
        $uow = $em->getUnitOfWork();
0 ignored issues
show
Bug introduced by
The method getUnitOfWork() does not exist on Doctrine\Persistence\ObjectManager. It seems like you code against a sub-type of said class. However, the method does not exist in Doctrine\Persistence\ObjectManagerDecorator. Are you sure you never get one of those? ( Ignorable by Annotation )

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

59
        /** @scrutinizer ignore-call */ 
60
        $uow = $em->getUnitOfWork();
Loading history...
60
61
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
62
            $this->gatherResourceAndAssociated($entity, 'created', $em, $uow);
63
        }
64
65
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
66
            $this->gatherResourceAndAssociated($entity, 'updated', $em, $uow);
67
        }
68
69
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
70
            $this->gatherResourceAndAssociated($entity, 'deleted', $em, $uow);
71
        }
72
73
        $this->collectUpdatedPageDataAndPositions();
74
    }
75
76
    public function postFlush(PostFlushEventArgs $eventArgs): void
77
    {
78
        $this->refreshUpdatedEntities($eventArgs->getObjectManager());
79
        $this->collectDynamicComponentPositionResources();
80
        $this->collectRelatedCollectionComponentResources();
81
        $this->purgeResources();
82
    }
83
84
    private function collectUpdatedPageDataAndPositions(): void
85
    {
86
        foreach ($this->updatedResources as $updatedResource) {
87
            $pageDataComponentMetadata = $this->pageDataProvider->findPageDataComponentMetadata($updatedResource);
88
            foreach ($pageDataComponentMetadata as $pageDataComponentMetadatum) {
89
                $pageDataResources = $pageDataComponentMetadatum->getPageDataResources();
90
                if (count($pageDataResources)) {
91
                    foreach ($pageDataResources as $pageDataResource) {
92
                        $this->collectUpdatedResource($pageDataResource, 'updated');
93
                        $this->addToPropagators($pageDataResource, 'updated');
94
                    }
95
96
                    $pageDataComponentProperties = $pageDataComponentMetadatum->getProperties();
97
                    $this->collectDynamicComponentPositionResources($pageDataComponentProperties->toArray());
98
                }
99
            }
100
        }
101
    }
102
103
    private function refreshUpdatedEntities(ObjectManager $om): void
104
    {
105
        foreach ($this->updatedResources as $updatedResource) {
106
            $data = $this->updatedResources[$updatedResource];
107
            if ('deleted' !== $data['type'] && $om->contains($updatedResource)) {
108
                $om->refresh($updatedResource);
109
                $this->addToPropagators($updatedResource, $data['type']);
110
            }
111
        }
112
    }
113
114
    private function gatherResourceAndAssociated(object $entity, string $type, ObjectManager $em, UnitOfWork $uow): void
115
    {
116
        $changeSet = $uow->getEntityChangeSet($entity);
117
        $this->collectUpdatedResource($entity, $type);
118
119
        $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
120
121
        if ($entity instanceof PageDataInterface) {
122
            $this->pageDataPropertiesChanged = array_keys($changeSet);
123
        }
124
125
        if ('updated' === $type) {
126
            $this->gatherUpdatedAssociatedEntities($associationMappings, $changeSet);
127
128
            return;
129
        }
130
131
        // created and deleted - full entity change, all properties to check
132
        // catch any related resources that may have changed backwards relation or database cascades
133
        $this->gatherAllAssociatedEntities($entity, $associationMappings);
134
    }
135
136
    private function gatherUpdatedAssociatedEntities(array $associationMappings, array $changeSet): void
137
    {
138
        foreach ($changeSet as $field => $values) {
139
            // detect whether changed field was an association
140
            if (!isset($associationMappings[$field])) {
141
                continue;
142
            }
143
144
            if (isset($associationMappings[$field]['inversedBy'])) {
145
                $notNullValues = array_filter($values);
146
                foreach ($notNullValues as $entityInverseValuesUpdated) {
147
                    // note: the resource may get removed if orphaned
148
                    $this->collectUpdatedResource($entityInverseValuesUpdated, 'updated');
149
                }
150
            }
151
        }
152
    }
153
154
    private function gatherAllAssociatedEntities(object $entity, array $associationMappings): void
155
    {
156
        foreach (array_keys($associationMappings) as $property) {
157
            if (
158
                !$this->propertyAccessor->isReadable($entity, $property) ||
159
                !$assocEntity = $this->propertyAccessor->getValue($entity, $property)
160
            ) {
161
                continue;
162
            }
163
164
            if ($assocEntity instanceof PersistentCollection) {
165
                foreach ($assocEntity as $oneToManyEntity) {
166
                    if (!$oneToManyEntity) {
167
                        continue;
168
                    }
169
                    $this->collectUpdatedResource($oneToManyEntity, 'updated');
170
                }
171
172
                return;
173
            }
174
            $this->collectUpdatedResource($assocEntity, 'updated');
175
        }
176
    }
177
178
    private function collectUpdatedResource($resource, string $type): void
179
    {
180
        if (!$resource) {
181
            return;
182
        }
183
        $this->addResourceIrisFromObject($resource, $type);
184
        $this->addToPropagators($resource, $type);
185
    }
186
187
    private function addResourceIrisFromObject($resource, string $type): void
188
    {
189
        if (
190
            isset($this->updatedResources[$resource]) &&
191
            $type !== 'deleted'
192
        ) {
193
            return;
194
        }
195
196
        try {
197
            $resourceClass = $this->resourceClassResolver->getResourceClass($resource);
198
        } catch (InvalidArgumentException $e) {
199
            return;
200
        }
201
202
        // collect get collection iris for clearing the collection components in the cache later
203
        if (!isset($this->updatedCollectionClassToIriMapping[$resourceClass])) {
204
            try {
205
                $collectionIri = $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, (new GetCollection())->withClass($resourceClass));
206
                $this->updatedCollectionClassToIriMapping[$resourceClass] = $collectionIri;
207
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
208
            }
209
        }
210
211
        // keep a record of all the resources we are triggering updates for related from the database changes
212
        $resourceIri = $this->iriConverter->getIriFromResource($resource);
213
214
        $this->updatedResources[$resource] = [
215
            'iri' => $resourceIri,
216
            'type' => $type,
217
            'resourceClass' => $resourceClass,
218
        ];
219
    }
220
221
    private function collectDynamicComponentPositionResources(array $pageDataPropertiesChanged = null): void
222
    {
223
        if (!$pageDataPropertiesChanged) {
224
            $pageDataPropertiesChanged = $this->pageDataPropertiesChanged;
225
        }
226
        if (0 === count($pageDataPropertiesChanged)) {
227
            return;
228
        }
229
        $positions = $this->positionRepository->findByPageDataProperties($pageDataPropertiesChanged);
230
231
        foreach ($positions as $position) {
232
            $this->collectUpdatedResource($position, 'updated');
233
            $this->addToPropagators($position, 'updated');
234
        }
235
    }
236
237
    private function collectRelatedCollectionComponentResources(): void
238
    {
239
        if (empty($this->updatedCollectionClassToIriMapping)) {
240
            return;
241
        }
242
243
        foreach ($this->updatedCollectionClassToIriMapping as $resourceIri) {
244
            $collections = $this->collectionRepository->findBy([
245
                'resourceIri' => $resourceIri,
246
            ]);
247
            foreach ($collections as $collection) {
248
                $this->collectUpdatedResource($collection, 'updated');
249
                $this->addToPropagators($collection, 'updated');
250
            }
251
        }
252
    }
253
254
    private function purgeResources(): void
255
    {
256
        foreach ($this->resourceChangedPropagators as $resourceChangedPropagator) {
257
            $resourceChangedPropagator->propagate();
258
        }
259
260
        $this->reset();
261
    }
262
263
    private function addToPropagators(object $item, string $type): void
264
    {
265
        foreach ($this->resourceChangedPropagators as $resourceChangedPropagator) {
266
            $resourceChangedPropagator->add($item, $type);
267
        }
268
    }
269
270
    private function reset(): void
271
    {
272
        $this->updatedResources = new \SplObjectStorage();
273
        $this->pageDataPropertiesChanged = [];
274
        $this->updatedCollectionClassToIriMapping = [];
275
    }
276
}
277