Passed
Push — main ( 341aa7...1aa4cc )
by Daniel
13:35
created

gatherUpdatedAssociatedEntities()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 13
ccs 0
cts 8
cp 0
crap 30
rs 9.6111
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[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 Silverback\ApiComponentsBundle\EventListener\Doctrine;
15
16
use ApiPlatform\Api\IriConverterInterface;
17
use ApiPlatform\Api\ResourceClassResolverInterface;
18
use ApiPlatform\Api\UrlGeneratorInterface;
19
use ApiPlatform\Exception\InvalidArgumentException;
20
use ApiPlatform\Metadata\GetCollection;
21
use Doctrine\Common\Util\ClassUtils;
22
use Doctrine\ORM\EntityRepository;
23
use Doctrine\ORM\Event\OnFlushEventArgs;
24
use Doctrine\ORM\Event\PostFlushEventArgs;
25
use Doctrine\ORM\PersistentCollection;
26
use Doctrine\ORM\UnitOfWork;
27
use Doctrine\Persistence\ManagerRegistry;
28
use Doctrine\Persistence\ObjectManager;
29
use Doctrine\Persistence\ObjectRepository;
30
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
31
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
32
use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition;
33
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
34
use Silverback\ApiComponentsBundle\HttpCache\ResourceChangedPropagatorInterface;
35
use Silverback\ApiComponentsBundle\Metadata\Provider\PageDataMetadataProvider;
36
use Silverback\ApiComponentsBundle\Repository\Core\ComponentPositionRepository;
37
use Symfony\Component\PropertyAccess\PropertyAccess;
38
use Symfony\Component\PropertyAccess\PropertyAccessor;
39
40
trait DoctrineResourceFlushTrait
41
{
42
    private PropertyAccessor $propertyAccessor;
43
    private ObjectRepository|EntityRepository $collectionRepository;
44
    private \SplObjectStorage $updatedResources;
45
    private array $pageDataPropertiesChanged = [];
46
    private array $updatedCollectionClassToIriMapping = [];
47
48
    public function __construct(
49
        private readonly IriConverterInterface $iriConverter,
50
        ManagerRegistry $entityManager,
51
        private readonly ResourceChangedPropagatorInterface $resourceChangedPropagator,
52
        private readonly ResourceClassResolverInterface $resourceClassResolver,
53
        private readonly PageDataMetadataProvider $pageDataMetadataProvider,
54
        private readonly PageDataProvider $pageDataProvider,
55
        private readonly ComponentPositionRepository $positionRepository
56
    ) {
57
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
58
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
59
        $this->reset();
60
    }
61
62
    public function onFlush(OnFlushEventArgs $eventArgs): void
63
    {
64
        $em = $eventArgs->getObjectManager();
65
        $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

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