PropagateUpdatesListener   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 111
dl 0
loc 242
ccs 0
cts 118
cp 0
rs 7.92
c 0
b 0
f 0
wmc 51

15 Methods

Rating   Name   Duplication   Size   Complexity  
A reset() 0 5 1
A purgeResources() 0 7 2
A collectDynamicComponentPositionResources() 0 13 4
A collectUpdatedResource() 0 7 2
A addToPropagators() 0 4 2
A collectRelatedCollectionComponentResources() 0 13 4
A addResourceIrisFromObject() 0 31 6
A refreshUpdatedEntities() 0 7 4
B gatherAllAssociatedEntities() 0 20 7
A collectUpdatedPageDataAndPositions() 0 14 5
A postFlush() 0 6 1
A __construct() 0 11 1
A onFlush() 0 18 4
A gatherResourceAndAssociated() 0 20 3
A gatherUpdatedAssociatedEntities() 0 13 5

How to fix   Complexity   

Complex Class

Complex classes like PropagateUpdatesListener often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PropagateUpdatesListener, and based on these observations, apply Extract Interface, too.

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\Exception\InvalidArgumentException;
17
use ApiPlatform\Metadata\Exception\InvalidArgumentException as LegacyInvalidArgumentException;
18
use ApiPlatform\Metadata\GetCollection;
19
use ApiPlatform\Metadata\IriConverterInterface;
20
use ApiPlatform\Metadata\ResourceClassResolverInterface;
21
use ApiPlatform\Metadata\UrlGeneratorInterface;
22
use Doctrine\Common\Util\ClassUtils;
23
use Doctrine\ORM\EntityRepository;
24
use Doctrine\ORM\Event\OnFlushEventArgs;
25
use Doctrine\ORM\Event\PostFlushEventArgs;
26
use Doctrine\ORM\PersistentCollection;
27
use Doctrine\ORM\UnitOfWork;
28
use Doctrine\Persistence\ManagerRegistry;
29
use Doctrine\Persistence\ObjectManager;
30
use Doctrine\Persistence\ObjectRepository;
31
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
32
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
33
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
34
use Silverback\ApiComponentsBundle\HttpCache\ResourceChangedPropagatorInterface;
35
use Silverback\ApiComponentsBundle\Repository\Core\ComponentPositionRepository;
36
use Symfony\Component\PropertyAccess\PropertyAccess;
37
use Symfony\Component\PropertyAccess\PropertyAccessor;
38
39
class PropagateUpdatesListener
40
{
41
    private PropertyAccessor $propertyAccessor;
42
    private ObjectRepository|EntityRepository $collectionRepository;
43
    private \SplObjectStorage $updatedResources;
44
    private array $pageDataPropertiesChanged = [];
45
    private array $updatedCollectionClassToIriMapping = [];
46
47
    /**
48
     * @param iterable|ResourceChangedPropagatorInterface[] $resourceChangedPropagators
49
     */
50
    public function __construct(
51
        private readonly IriConverterInterface $iriConverter,
52
        ManagerRegistry $entityManager,
53
        private readonly iterable $resourceChangedPropagators,
54
        private readonly ResourceClassResolverInterface $resourceClassResolver,
55
        private readonly PageDataProvider $pageDataProvider,
56
        private readonly ComponentPositionRepository $positionRepository
57
    ) {
58
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
59
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
60
        $this->reset();
61
    }
62
63
    public function onFlush(OnFlushEventArgs $eventArgs): void
64
    {
65
        $em = $eventArgs->getObjectManager();
66
        $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

66
        /** @scrutinizer ignore-call */ 
67
        $uow = $em->getUnitOfWork();
Loading history...
67
68
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
69
            $this->gatherResourceAndAssociated($entity, 'created', $em, $uow);
70
        }
71
72
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
73
            $this->gatherResourceAndAssociated($entity, 'updated', $em, $uow);
74
        }
75
76
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
77
            $this->gatherResourceAndAssociated($entity, 'deleted', $em, $uow);
78
        }
79
80
        $this->collectUpdatedPageDataAndPositions();
81
    }
82
83
    public function postFlush(PostFlushEventArgs $eventArgs): void
84
    {
85
        $this->refreshUpdatedEntities($eventArgs->getObjectManager());
86
        $this->collectDynamicComponentPositionResources();
87
        $this->collectRelatedCollectionComponentResources();
88
        $this->purgeResources();
89
    }
90
91
    private function collectUpdatedPageDataAndPositions(): void
92
    {
93
        foreach ($this->updatedResources as $updatedResource) {
94
            $pageDataComponentMetadata = $this->pageDataProvider->findPageDataComponentMetadata($updatedResource);
95
            foreach ($pageDataComponentMetadata as $pageDataComponentMetadatum) {
96
                $pageDataResources = $pageDataComponentMetadatum->getPageDataResources();
97
                if (\count($pageDataResources)) {
98
                    foreach ($pageDataResources as $pageDataResource) {
99
                        $this->collectUpdatedResource($pageDataResource, 'updated');
100
                        $this->addToPropagators($pageDataResource, 'updated');
101
                    }
102
103
                    $pageDataComponentProperties = $pageDataComponentMetadatum->getProperties();
104
                    $this->collectDynamicComponentPositionResources($pageDataComponentProperties->toArray());
105
                }
106
            }
107
        }
108
    }
109
110
    private function refreshUpdatedEntities(ObjectManager $om): void
111
    {
112
        foreach ($this->updatedResources as $updatedResource) {
113
            $data = $this->updatedResources[$updatedResource];
114
            if ('deleted' !== $data['type'] && $om->contains($updatedResource)) {
115
                $om->refresh($updatedResource);
116
                $this->addToPropagators($updatedResource, $data['type']);
117
            }
118
        }
119
    }
120
121
    private function gatherResourceAndAssociated(object $entity, string $type, ObjectManager $em, UnitOfWork $uow): void
122
    {
123
        $changeSet = $uow->getEntityChangeSet($entity);
124
        $this->collectUpdatedResource($entity, $type);
125
126
        $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
127
128
        if ($entity instanceof PageDataInterface) {
129
            $this->pageDataPropertiesChanged = array_keys($changeSet);
130
        }
131
132
        if ('updated' === $type) {
133
            $this->gatherUpdatedAssociatedEntities($associationMappings, $changeSet);
134
135
            return;
136
        }
137
138
        // created and deleted - full entity change, all properties to check
139
        // catch any related resources that may have changed backwards relation or database cascades
140
        $this->gatherAllAssociatedEntities($entity, $associationMappings);
141
    }
142
143
    private function gatherUpdatedAssociatedEntities(array $associationMappings, array $changeSet): void
144
    {
145
        foreach ($changeSet as $field => $values) {
146
            // detect whether changed field was an association
147
            if (!isset($associationMappings[$field])) {
148
                continue;
149
            }
150
151
            if (isset($associationMappings[$field]['inversedBy'])) {
152
                $notNullValues = array_filter($values);
153
                foreach ($notNullValues as $entityInverseValuesUpdated) {
154
                    // note: the resource may get removed if orphaned
155
                    $this->collectUpdatedResource($entityInverseValuesUpdated, 'updated');
156
                }
157
            }
158
        }
159
    }
160
161
    private function gatherAllAssociatedEntities(object $entity, array $associationMappings): void
162
    {
163
        foreach (array_keys($associationMappings) as $property) {
164
            if (
165
                !$this->propertyAccessor->isReadable($entity, $property)
166
                || !$assocEntity = $this->propertyAccessor->getValue($entity, $property)
167
            ) {
168
                continue;
169
            }
170
171
            if ($assocEntity instanceof PersistentCollection) {
172
                foreach ($assocEntity as $oneToManyEntity) {
173
                    if (!$oneToManyEntity) {
174
                        continue;
175
                    }
176
                    $this->collectUpdatedResource($oneToManyEntity, 'updated');
177
                }
178
                continue;
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->addToPropagators($resource, $type);
191
    }
192
193
    private function addResourceIrisFromObject($resource, string $type): void
194
    {
195
        if (
196
            isset($this->updatedResources[$resource])
197
            && 'deleted' !== $type
198
        ) {
199
            return;
200
        }
201
202
        try {
203
            $resourceClass = $this->resourceClassResolver->getResourceClass($resource);
204
        } catch (InvalidArgumentException|LegacyInvalidArgumentException) {
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|LegacyInvalidArgumentException) {
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
        if (0 === \count($pageDataPropertiesChanged)) {
233
            return;
234
        }
235
        $positions = $this->positionRepository->findByPageDataProperties($pageDataPropertiesChanged);
236
237
        foreach ($positions as $position) {
238
            $this->collectUpdatedResource($position, 'updated');
239
            $this->addToPropagators($position, 'updated');
240
        }
241
    }
242
243
    private function collectRelatedCollectionComponentResources(): void
244
    {
245
        if (empty($this->updatedCollectionClassToIriMapping)) {
246
            return;
247
        }
248
249
        foreach ($this->updatedCollectionClassToIriMapping as $resourceIri) {
250
            $collections = $this->collectionRepository->findBy([
251
                'resourceIri' => $resourceIri,
252
            ]);
253
            foreach ($collections as $collection) {
254
                $this->collectUpdatedResource($collection, 'updated');
255
                $this->addToPropagators($collection, 'updated');
256
            }
257
        }
258
    }
259
260
    private function purgeResources(): void
261
    {
262
        foreach ($this->resourceChangedPropagators as $resourceChangedPropagator) {
263
            $resourceChangedPropagator->propagate();
264
        }
265
266
        $this->reset();
267
    }
268
269
    private function addToPropagators(object $item, string $type): void
270
    {
271
        foreach ($this->resourceChangedPropagators as $resourceChangedPropagator) {
272
            $resourceChangedPropagator->add($item, $type);
273
        }
274
    }
275
276
    private function reset(): void
277
    {
278
        $this->updatedResources = new \SplObjectStorage();
279
        $this->pageDataPropertiesChanged = [];
280
        $this->updatedCollectionClassToIriMapping = [];
281
    }
282
}
283