Passed
Push — main ( 964870...8374cd )
by Daniel
08:40 queued 03:10
created

PurgeHttpCacheListener   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 81
dl 0
loc 179
ccs 0
cts 80
cp 0
rs 9.76
c 1
b 0
f 0
wmc 33

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getAssociationMappings() 0 3 1
A purgeTags() 0 8 2
A onFlush() 0 18 4
A postFlush() 0 4 1
A __construct() 0 8 1
A addResourceClass() 0 9 3
A gatherRelationResourceClasses() 0 12 5
A addTagsFor() 0 18 5
A preUpdate() 0 15 3
A addTagForItem() 0 7 3
A purgeCollectionResources() 0 22 5
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 Symfony\Component\PropertyAccess\PropertyAccess;
34
use Symfony\Component\PropertyAccess\PropertyAccessor;
35
36
/**
37
 * Purges desired resources on when doctrine is flushed from the proxy cache.
38
 *
39
 * @author Daniel West <[email protected]>
40
 *
41
 * @experimental
42
 */
43
class PurgeHttpCacheListener
44
{
45
    private PurgerInterface $purger;
46
    private IriConverterInterface $iriConverter;
47
    private ResourceClassResolverInterface $resourceClassResolver;
48
    private array $resourceIris = [];
49
    private array $tags = [];
50
    private PropertyAccessor $propertyAccessor;
51
    private ObjectRepository|EntityRepository $collectionRepository;
52
53
    public function __construct(PurgerInterface $purger, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ManagerRegistry $entityManager)
54
    {
55
        $this->purger = $purger;
56
        $this->iriConverter = $iriConverter;
57
        $this->resourceClassResolver = $resourceClassResolver;
58
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
59
60
        $this->collectionRepository = $entityManager->getRepository(Collection::class);
61
    }
62
63
    /**
64
     * Collects modified resources so we can check if any collection components need purging.
65
     *
66
     * Based on:
67
     *
68
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
69
     */
70
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
71
    {
72
        $object = $eventArgs->getObject();
73
        $this->addResourceClass($object);
74
75
        $changeSet = $eventArgs->getEntityChangeSet();
76
        $associationMappings = $this->getAssociationMappings($eventArgs->getEntityManager(), $eventArgs->getObject());
77
78
        foreach ($changeSet as $key => $value) {
79
            if (!isset($associationMappings[$key])) {
80
                continue;
81
            }
82
83
            $this->addResourceClass($value[0]);
84
            $this->addResourceClass($value[1]);
85
        }
86
    }
87
88
    public function onFlush(OnFlushEventArgs $eventArgs): void
89
    {
90
        $em = $eventArgs->getEntityManager();
91
        $uow = $em->getUnitOfWork();
92
93
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
94
            $this->addResourceClass($entity);
95
            $this->gatherRelationResourceClasses($em, $entity);
96
        }
97
98
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
99
            $this->addResourceClass($entity);
100
            $this->gatherRelationResourceClasses($em, $entity);
101
        }
102
103
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
104
            $this->addResourceClass($entity);
105
            $this->gatherRelationResourceClasses($em, $entity);
106
        }
107
    }
108
109
    /**
110
     * Purges tags collected during this request, and clears the tag list.
111
     *
112
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
113
     */
114
    public function postFlush(): void
115
    {
116
        $this->purgeCollectionResources();
117
        $this->purgeTags();
118
    }
119
120
    private function purgeCollectionResources(): void
121
    {
122
        if (empty($this->resourceIris)) {
123
            return;
124
        }
125
126
        $collectionIris = [];
127
        foreach ($this->resourceIris as $resourceIri) {
128
            $collections = $this->collectionRepository->findBy([
129
                'resourceIri' => $resourceIri,
130
            ]);
131
            foreach ($collections as $collection) {
132
                $collectionIris[] = $this->iriConverter->getIriFromResource($collection);
133
            }
134
        }
135
136
        $this->resourceIris = [];
137
        if (empty($collectionIris)) {
138
            return;
139
        }
140
141
        $this->purger->purge($collectionIris);
142
    }
143
144
    private function purgeTags(): void
145
    {
146
        if (empty($this->tags)) {
147
            return;
148
        }
149
150
        $this->purger->purge(array_values($this->tags));
151
        $this->tags = [];
152
    }
153
154
    private function addResourceClass($entity): void
155
    {
156
        try {
157
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
158
            $resourceIri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, (new GetCollection())->withClass($resourceClass));
159
            if (!\in_array($resourceIri, $this->resourceIris, true)) {
160
                $this->resourceIris[] = $resourceIri;
161
            }
162
        } 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...
163
        }
164
    }
165
166
    private function gatherRelationResourceClasses(EntityManagerInterface $em, $entity): void
167
    {
168
        $associationMappings = $this->getAssociationMappings($em, $entity);
169
        foreach (array_keys($associationMappings) as $property) {
170
            if ($this->propertyAccessor->isReadable($entity, $property)) {
171
                $value = $this->propertyAccessor->getValue($entity, $property);
172
                if ($value instanceof PersistentCollection) {
173
                    foreach ($value as $item) {
174
                        $this->addResourceClass($item);
175
                    }
176
                } else {
177
                    $this->addResourceClass($value);
178
                }
179
            }
180
        }
181
    }
182
183
    private function getAssociationMappings(EntityManagerInterface $em, $entity): array
184
    {
185
        return $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
186
    }
187
188
    /**
189
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
190
     */
191
    public function addTagsFor($value): void
192
    {
193
        if (!$value) {
194
            return;
195
        }
196
197
        if (!is_iterable($value)) {
198
            $this->addTagForItem($value);
199
200
            return;
201
        }
202
203
        if ($value instanceof PersistentCollection) {
204
            $value = clone $value;
205
        }
206
207
        foreach ($value as $v) {
208
            $this->addTagForItem($v);
209
        }
210
    }
211
212
    /**
213
     * @see \ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener
214
     */
215
    private function addTagForItem($value): void
216
    {
217
        try {
218
            $iri = $this->iriConverter->getIriFromResource($value);
219
            $this->tags[$iri] = $iri;
220
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
221
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
222
        }
223
    }
224
}
225