Test Failed
Push — main ( 27c872...388b7c )
by Daniel
16:28 queued 12s
created

purgePositionsWithPageDataProperties()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
nc 3
nop 0
dl 0
loc 11
ccs 0
cts 5
cp 0
crap 12
rs 10
c 1
b 0
f 0
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\Exception\OperationNotFoundException;
21
use ApiPlatform\Exception\RuntimeException;
22
use ApiPlatform\HttpCache\PurgerInterface;
23
use ApiPlatform\Metadata\GetCollection;
24
use Doctrine\Common\Util\ClassUtils;
25
use Doctrine\ORM\EntityManagerInterface;
26
use Doctrine\ORM\EntityRepository;
27
use Doctrine\ORM\Event\OnFlushEventArgs;
28
use Doctrine\ORM\Event\PreUpdateEventArgs;
29
use Doctrine\ORM\PersistentCollection;
30
use Doctrine\Persistence\ManagerRegistry;
31
use Doctrine\Persistence\ObjectRepository;
32
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
33
use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition;
34
use Silverback\ApiComponentsBundle\Entity\Core\PageDataInterface;
35
use Symfony\Component\PropertyAccess\PropertyAccess;
36
use Symfony\Component\PropertyAccess\PropertyAccessor;
37
38
/**
39
 * Purges desired resources on when doctrine is flushed from the proxy cache.
40
 *
41
 * @author Daniel West <[email protected]>
42
 *
43
 * @experimental
44
 */
45
class PurgeHttpCacheListener
46
{
47
    private PurgerInterface $purger;
48
    private IriConverterInterface $iriConverter;
49
    private ResourceClassResolverInterface $resourceClassResolver;
50
    private array $resourceIris = [];
51
    private array $tags = [];
52
    private array $pageDataPropertiesChanged = [];
53
    private PropertyAccessor $propertyAccessor;
54
    private ObjectRepository|EntityRepository $collectionRepository;
55
    private ObjectRepository|EntityRepository $positionRepository;
56
57
    public function __construct(PurgerInterface $purger, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ManagerRegistry $entityManager)
58
    {
59
        $this->purger = $purger;
60
        $this->iriConverter = $iriConverter;
61
        $this->resourceClassResolver = $resourceClassResolver;
62
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
63
64
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
65
        $this->positionRepository = $entityManager->getRepository(ComponentPosition::class);
66
    }
67
68
    /**
69
     * Collects modified resources so we can check if any collection components need purging.
70
     *
71
     * Based on:
72
     *
73
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
74
     */
75
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
76
    {
77
        $object = $eventArgs->getObject();
78
        $this->addResourceClass($object);
79
80
        $changeSet = $eventArgs->getEntityChangeSet();
81
        $associationMappings = $this->getAssociationMappings($eventArgs->getEntityManager(), $eventArgs->getObject());
82
83
        if ($object instanceof PageDataInterface) {
84
            $this->pageDataPropertiesChanged = array_keys($changeSet);
85
        }
86
87
        foreach ($changeSet as $field => $value) {
88
            if (!isset($associationMappings[$field])) {
89
                continue;
90
            }
91
92
            $this->addResourceClass($value[0]);
93
            $this->addResourceClass($value[1]);
94
        }
95
    }
96
97
    public function onFlush(OnFlushEventArgs $eventArgs): void
98
    {
99
        $em = $eventArgs->getEntityManager();
100
        $uow = $em->getUnitOfWork();
101
102
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
103
            $this->addResourceClass($entity);
104
            $this->gatherRelationResourceClasses($em, $entity);
105
        }
106
107
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
108
            $this->addResourceClass($entity);
109
            $this->gatherRelationResourceClasses($em, $entity);
110
        }
111
112
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
113
            $this->addResourceClass($entity);
114
            $this->gatherRelationResourceClasses($em, $entity);
115
        }
116
    }
117
118
    /**
119
     * Purges tags collected during this request, and clears the tag list.
120
     *
121
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
122
     */
123
    public function postFlush(): void
124
    {
125
        $this->purgePositionsWithPageDataProperties();
126
        $this->purgeCollectionResources();
127
        $this->purgeTags();
128
    }
129
130
    private function purgePositionsWithPageDataProperties(): void
131
    {
132
        foreach ($this->pageDataPropertiesChanged as $pageDataProperty) {
133
            $positions = $this->positionRepository->findBy([
134
                'pageDataProperty' => $pageDataProperty,
135
            ]);
136
            $positionIris = [];
137
            foreach ($positions as $position) {
138
                $positionIris[] = $this->iriConverter->getIriFromResource($position);
139
            }
140
            $this->purger->purge($positionIris);
141
        }
142
    }
143
144
    private function purgeCollectionResources(): void
145
    {
146
        if (empty($this->resourceIris)) {
147
            return;
148
        }
149
150
        $collectionIris = [];
151
        foreach ($this->resourceIris as $resourceIri) {
152
            $collections = $this->collectionRepository->findBy([
153
                'resourceIri' => $resourceIri,
154
            ]);
155
            foreach ($collections as $collection) {
156
                $collectionIris[] = $this->iriConverter->getIriFromResource($collection);
157
            }
158
        }
159
160
        $this->resourceIris = [];
161
        if (empty($collectionIris)) {
162
            return;
163
        }
164
165
        $this->purger->purge($collectionIris);
166
    }
167
168
    private function purgeTags(): void
169
    {
170
        if (empty($this->tags)) {
171
            return;
172
        }
173
174
        $this->purger->purge(array_values($this->tags));
175
        $this->tags = [];
176
    }
177
178
    private function addResourceClass($entity): void
179
    {
180
        if (null === $entity) {
181
            return;
182
        }
183
184
        try {
185
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
186
            $resourceIri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, (new GetCollection())->withClass($resourceClass));
187
            if (!\in_array($resourceIri, $this->resourceIris, true)) {
188
                $this->resourceIris[] = $resourceIri;
189
            }
190
        } catch (OperationNotFoundException|InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
191
        }
192
    }
193
194
    private function gatherRelationResourceClasses(EntityManagerInterface $em, $entity): void
195
    {
196
        $associationMappings = $this->getAssociationMappings($em, $entity);
197
        foreach (array_keys($associationMappings) as $property) {
198
            if ($this->propertyAccessor->isReadable($entity, $property)) {
199
                $value = $this->propertyAccessor->getValue($entity, $property);
200
                if ($value instanceof PersistentCollection) {
201
                    foreach ($value as $item) {
202
                        $this->addResourceClass($item);
203
                    }
204
                } else {
205
                    $this->addResourceClass($value);
206
                }
207
            }
208
        }
209
    }
210
211
    private function getAssociationMappings(EntityManagerInterface $em, $entity): array
212
    {
213
        return $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
214
    }
215
216
    /**
217
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
218
     */
219
    public function addTagsFor($value): void
220
    {
221
        if (!$value) {
222
            return;
223
        }
224
225
        if (!is_iterable($value)) {
226
            $this->addTagForItem($value);
227
228
            return;
229
        }
230
231
        if ($value instanceof PersistentCollection) {
232
            $value = clone $value;
233
        }
234
235
        foreach ($value as $v) {
236
            $this->addTagForItem($v);
237
        }
238
    }
239
240
    /**
241
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
242
     */
243
    private function addTagForItem($value): void
244
    {
245
        try {
246
            $iri = $this->iriConverter->getIriFromResource($value);
247
            $this->tags[$iri] = $iri;
248
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
249
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
250
        }
251
    }
252
}
253