Test Failed
Pull Request — main (#140)
by Daniel
04:22
created

PurgeHttpCacheListener   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 171
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 75
dl 0
loc 171
ccs 0
cts 20
cp 0
rs 9.92
c 1
b 0
f 0
wmc 31

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