Completed
Push — master ( 8ed3bd...5a8135 )
by Kévin
03:59
created

PurgeHttpCacheListener::postFlush()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
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)
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)
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()
101
    {
102
        $this->purger->purge($this->tags);
103
        $this->tags = [];
104
    }
105
106
    private function gatherResourceAndItemTags($entity, bool $purgeItem)
107
    {
108
        try {
109
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
110
        } catch (InvalidArgumentException $e) {
111
            return;
112
        }
113
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
    }
121
122
    private function gatherRelationTags(EntityManagerInterface $em, $entity)
123
    {
124
        $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings();
125
        foreach (array_keys($associationMappings) as $property) {
126
            $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
127
        }
128
    }
129
130
    private function addTagsFor($value)
131
    {
132
        if (!is_array($value) && !$value instanceof \Traversable) {
133
            $this->addTagForItem($value);
134
135
            return;
136
        }
137
138
        if ($value instanceof PersistentCollection) {
139
            $value = clone $value;
140
        }
141
142
        foreach ($value as $v) {
143
            $this->addTagForItem($v);
144
        }
145
    }
146
147
    private function addTagForItem($value)
148
    {
149
        try {
150
            $iri = $this->iriConverter->getIriFromItem($value);
151
            $this->tags[$iri] = $iri;
152
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
153
        } catch (RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
154
        }
155
    }
156
}
157