Passed
Push — main ( 6f7970...605cbe )
by Daniel
03:52
created

gatherResourceAndAssociated()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 9
c 0
b 0
f 0
nc 4
nop 4
dl 0
loc 20
ccs 0
cts 10
cp 0
crap 12
rs 9.9666
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\Entity\Component\Collection;
31
use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition;
32
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
33
use Silverback\ApiComponentsBundle\HttpCache\ResourceChangedPropagatorInterface;
34
use Symfony\Component\PropertyAccess\PropertyAccess;
35
use Symfony\Component\PropertyAccess\PropertyAccessor;
36
37
trait DoctrineResourceFlushTrait
38
{
39
    private PropertyAccessor $propertyAccessor;
40
    private ObjectRepository|EntityRepository $collectionRepository;
41
    private ObjectRepository|EntityRepository $positionRepository;
42
43
    private \SplObjectStorage $updatedResources;
44
    private array $pageDataPropertiesChanged = [];
45
    private array $updatedCollectionClassToIriMapping = [];
46
47
    public function __construct(
48
        private readonly IriConverterInterface $iriConverter,
49
        ManagerRegistry $entityManager,
50
        private readonly ResourceChangedPropagatorInterface $resourceChangedPropagator,
51
        private readonly ResourceClassResolverInterface $resourceClassResolver
52
    ) {
53
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
54
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
55
        $this->positionRepository = $entityManager->getRepository(ComponentPosition::class);
56
        $this->reset();
57
    }
58
59
    public function onFlush(OnFlushEventArgs $eventArgs): void
60
    {
61
        $em = $eventArgs->getObjectManager();
62
        $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

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