• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 14008635868

22 Mar 2025 12:39PM UTC coverage: 8.52% (+0.005%) from 8.515%
14008635868

Pull #7042

github

web-flow
Merge fdd88ef56 into 47a6dffbb
Pull Request #7042: Purge parent collections in inheritance cases

4 of 9 new or added lines in 1 file covered. (44.44%)

540 existing lines in 57 files now uncovered.

13394 of 157210 relevant lines covered (8.52%)

22.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.38
/src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Symfony\Doctrine\EventListener;
15

16
use ApiPlatform\HttpCache\PurgerInterface;
17
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
18
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
19
use ApiPlatform\Metadata\Exception\RuntimeException;
20
use ApiPlatform\Metadata\GetCollection;
21
use ApiPlatform\Metadata\IriConverterInterface;
22
use ApiPlatform\Metadata\ResourceClassResolverInterface;
23
use ApiPlatform\Metadata\UrlGeneratorInterface;
24
use ApiPlatform\Metadata\Util\ClassInfoTrait;
25
use Doctrine\ORM\EntityManagerInterface;
26
use Doctrine\ORM\Event\OnFlushEventArgs;
27
use Doctrine\ORM\Event\PreUpdateEventArgs;
28
use Doctrine\ORM\Mapping\AssociationMapping;
29
use Doctrine\ORM\PersistentCollection;
30
use Symfony\Component\PropertyAccess\PropertyAccess;
31
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
32

33
/**
34
 * Purges responses containing modified entities from the proxy cache.
35
 *
36
 * @author Kévin Dunglas <dunglas@gmail.com>
37
 */
38
final class PurgeHttpCacheListener
39
{
40
    use ClassInfoTrait;
41
    private readonly PropertyAccessorInterface $propertyAccessor;
42
    private array $tags = [];
43

44
    public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null)
45
    {
46
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
652✔
47
    }
48

49
    /**
50
     * Collects tags from the previous and the current version of the updated entities to purge related documents.
51
     */
52
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
53
    {
UNCOV
54
        $object = $eventArgs->getObject();
57✔
UNCOV
55
        $this->gatherResourceAndItemTags($object, true);
57✔
56

UNCOV
57
        $changeSet = $eventArgs->getEntityChangeSet();
57✔
58
        // @phpstan-ignore-next-line
UNCOV
59
        $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
57✔
UNCOV
60
        $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings();
57✔
61

UNCOV
62
        foreach ($changeSet as $key => $value) {
57✔
UNCOV
63
            if (!isset($associationMappings[$key])) {
56✔
UNCOV
64
                continue;
41✔
65
            }
66

UNCOV
67
            $this->addTagsFor($value[0]);
18✔
UNCOV
68
            $this->addTagsFor($value[1]);
18✔
69
        }
70
    }
71

72
    /**
73
     * Collects tags from inserted and deleted entities, including relations.
74
     */
75
    public function onFlush(OnFlushEventArgs $eventArgs): void
76
    {
77
        // @phpstan-ignore-next-line
78
        $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
652✔
79
        $uow = $em->getUnitOfWork();
652✔
80

81
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
652✔
82
            $this->gatherResourceAndItemTags($entity, false);
602✔
83
            $this->gatherParentResourceTags($em, $entity);
602✔
84
            $this->gatherRelationTags($em, $entity);
602✔
85
        }
86

87
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
652✔
UNCOV
88
            $this->gatherResourceAndItemTags($entity, true);
57✔
UNCOV
89
            $this->gatherRelationTags($em, $entity);
57✔
90
        }
91

92
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
652✔
UNCOV
93
            $this->gatherResourceAndItemTags($entity, true);
14✔
UNCOV
94
            $this->gatherRelationTags($em, $entity);
14✔
95
        }
96
    }
97

98
    /**
99
     * Purges tags collected during this request, and clears the tag list.
100
     */
101
    public function postFlush(): void
102
    {
103
        if (empty($this->tags)) {
652✔
104
            return;
25✔
105
        }
106

107
        $this->purger->purge(array_values($this->tags));
631✔
108

109
        $this->tags = [];
631✔
110
    }
111

112
    private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void
113
    {
114
        try {
115
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
646✔
116
            $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection());
634✔
117
            $this->tags[$iri] = $iri;
627✔
118

119
            if ($purgeItem) {
627✔
120
                $this->addTagForItem($entity);
627✔
121
            }
122
        } catch (OperationNotFoundException|InvalidArgumentException) {
30✔
123
        }
124
    }
125

126
    private function gatherParentResourceTags(EntityManagerInterface $em, object $entity): void
127
    {
128
        $classMetadata = $em->getClassMetadata($entity::class);
602✔
129

130
        if ($classMetadata->isInheritanceTypeNone()) {
602✔
131
            return;
594✔
132
        }
133

NEW
134
        foreach ($classMetadata->parentClasses as $parentClass) {
10✔
NEW
135
            if ($this->resourceClassResolver->isResourceClass($parentClass)) {
10✔
136
                try {
NEW
137
                    $iri = $this->iriConverter->getIriFromResource($parentClass, UrlGeneratorInterface::ABS_PATH, new GetCollection());
10✔
NEW
138
                    $this->tags[$iri] = $iri;
10✔
NEW
139
                } catch (OperationNotFoundException|InvalidArgumentException) {
×
140
                }
141
            }
142
        }
143
    }
144

145
    private function gatherRelationTags(EntityManagerInterface $em, object $entity): void
146
    {
147
        $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings();
646✔
148
        /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */
149
        foreach ($associationMappings as $property => $associationMapping) {
646✔
150
            if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) {
306✔
151
                return;
21✔
152
            }
153

154
            if (
155
                \is_array($associationMapping)
299✔
156
                && \array_key_exists('targetEntity', $associationMapping)
299✔
157
                && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) {
299✔
158
                return;
×
159
            }
160

161
            if ($this->propertyAccessor->isReadable($entity, $property)) {
299✔
162
                $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
296✔
163
            }
164
        }
165
    }
166

167
    private function addTagsFor(mixed $value): void
168
    {
169
        if (!$value || \is_scalar($value)) {
296✔
170
            return;
208✔
171
        }
172

173
        if (!is_iterable($value)) {
282✔
174
            $this->addTagForItem($value);
164✔
175

176
            return;
164✔
177
        }
178

179
        if ($value instanceof PersistentCollection) {
230✔
180
            $value = clone $value;
230✔
181
        }
182

183
        foreach ($value as $v) {
230✔
UNCOV
184
            $this->addTagForItem($v);
68✔
185
        }
186
    }
187

188
    private function addTagForItem(mixed $value): void
189
    {
190
        if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) {
220✔
191
            return;
×
192
        }
193

194
        try {
195
            $iri = $this->iriConverter->getIriFromResource($value);
220✔
UNCOV
196
            $this->tags[$iri] = $iri;
117✔
197
        } catch (RuntimeException|InvalidArgumentException) {
116✔
198
        }
199
    }
200
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc