Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

Doctrine/EventListener/PurgeHttpCacheListener.php (2 issues)

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[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 ApiPlatform\Core\Bridge\Doctrine\EventListener;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
18
use ApiPlatform\Core\Exception\InvalidArgumentException;
19
use ApiPlatform\Core\Exception\RuntimeException;
20
use ApiPlatform\Core\HttpCache\PurgerInterface;
21
use Doctrine\Common\Util\ClassUtils;
22
use Doctrine\ORM\EntityManagerInterface;
23
use Doctrine\ORM\Event\OnFlushEventArgs;
24
use Doctrine\ORM\Event\PreUpdateEventArgs;
25
use Doctrine\ORM\PersistentCollection;
26
use Symfony\Component\PropertyAccess\PropertyAccess;
27
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
28
29
/**
30
 * Purges responses containing modified entities from the proxy cache.
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 *
34
 * @experimental
35
 */
36
final class PurgeHttpCacheListener
37
{
38
    private $purger;
39
    private $iriConverter;
40
    private $resourceClassResolver;
41
    private $propertyAccessor;
42
    private $tags = [];
43
44
    public function __construct(PurgerInterface $purger, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null)
45
    {
46
        $this->purger = $purger;
47
        $this->iriConverter = $iriConverter;
48
        $this->resourceClassResolver = $resourceClassResolver;
49
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
50
    }
51
52
    /**
53
     * Collects tags from the previous and the current version of the updated entities to purge related documents.
54
     */
55
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
56
    {
57
        $object = $eventArgs->getObject();
58
        $this->gatherResourceAndItemTags($object, true);
59
60
        $changeSet = $eventArgs->getEntityChangeSet();
61
        $associationMappings = $eventArgs->getEntityManager()->getClassMetadata(ClassUtils::getClass($eventArgs->getObject()))->getAssociationMappings();
62
63
        foreach ($changeSet as $key => $value) {
64
            if (!isset($associationMappings[$key])) {
65
                continue;
66
            }
67
68
            $this->addTagsFor($value[0]);
69
            $this->addTagsFor($value[1]);
70
        }
71
    }
72
73
    /**
74
     * Collects tags from inserted and deleted entities, including relations.
75
     */
76
    public function onFlush(OnFlushEventArgs $eventArgs): void
77
    {
78
        $em = $eventArgs->getEntityManager();
79
        $uow = $em->getUnitOfWork();
80
81
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
82
            $this->gatherResourceAndItemTags($entity, false);
83
            $this->gatherRelationTags($em, $entity);
84
        }
85
86
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
87
            $this->gatherResourceAndItemTags($entity, true);
88
            $this->gatherRelationTags($em, $entity);
89
        }
90
91
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
92
            $this->gatherResourceAndItemTags($entity, true);
93
            $this->gatherRelationTags($em, $entity);
94
        }
95
    }
96
97
    /**
98
     * Purges tags collected during this request, and clears the tag list.
99
     */
100
    public function postFlush(): void
101
    {
102
        if (empty($this->tags)) {
103
            return;
104
        }
105
106
        $this->purger->purge($this->tags);
107
        $this->tags = [];
108
    }
109
110
    private function gatherResourceAndItemTags($entity, bool $purgeItem): void
111
    {
112
        try {
113
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
114
            $iri = $this->iriConverter->getIriFromResourceClass($resourceClass);
115
            $this->tags[$iri] = $iri;
116
            if ($purgeItem) {
117
                $iri = $this->iriConverter->getIriFromItem($entity);
118
                $this->tags[$iri] = $iri;
119
            }
120
        } catch (InvalidArgumentException $e) {
121
            return;
122
        }
123
    }
124
125
    private function gatherRelationTags(EntityManagerInterface $em, $entity): void
126
    {
127
        $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
128
        foreach (array_keys($associationMappings) as $property) {
129
            $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
130
        }
131
    }
132
133
    private function addTagsFor($value): void
134
    {
135
        if (!$value) {
136
            return;
137
        }
138
139
        if (!is_iterable($value)) {
140
            $this->addTagForItem($value);
141
142
            return;
143
        }
144
145
        if ($value instanceof PersistentCollection) {
146
            $value = clone $value;
147
        }
148
149
        foreach ($value as $v) {
150
            $this->addTagForItem($v);
151
        }
152
    }
153
154
    private function addTagForItem($value): void
155
    {
156
        try {
157
            $iri = $this->iriConverter->getIriFromItem($value);
158
            $this->tags[$iri] = $iri;
159
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
160
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
161
        }
162
    }
163
}
164