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

api-platform / core / 20847864477

09 Jan 2026 09:47AM UTC coverage: 29.1% (+0.005%) from 29.095%
20847864477

Pull #7649

github

web-flow
Merge b342dd5db into d640d106b
Pull Request #7649: feat(validator): uuid/ulid parameter validation

0 of 4 new or added lines in 1 file covered. (0.0%)

15050 existing lines in 491 files now uncovered.

16996 of 58406 relevant lines covered (29.1%)

81.8 hits per line

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

80.68
/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\GetCollection;
20
use ApiPlatform\Metadata\IriConverterInterface;
21
use ApiPlatform\Metadata\ResourceClassResolverInterface;
22
use ApiPlatform\Metadata\UrlGeneratorInterface;
23
use ApiPlatform\Metadata\Util\ClassInfoTrait;
24
use Doctrine\ORM\EntityManagerInterface;
25
use Doctrine\ORM\Event\OnFlushEventArgs;
26
use Doctrine\ORM\Event\PreUpdateEventArgs;
27
use Doctrine\ORM\Mapping\AssociationMapping;
28
use Doctrine\ORM\PersistentCollection;
29
use Symfony\Component\ObjectMapper\Attribute\Map;
30
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
31
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
32
use Symfony\Component\PropertyAccess\PropertyAccess;
33
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
34

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

46
    private array $scheduledInsertions = [];
47

48
    public function __construct(private readonly PurgerInterface $purger,
49
        private readonly IriConverterInterface $iriConverter,
50
        private readonly ResourceClassResolverInterface $resourceClassResolver,
51
        ?PropertyAccessorInterface $propertyAccessor = null,
52
        private readonly ?ObjectMapperInterface $objectMapper = null,
53
        private readonly ?ObjectMapperMetadataFactoryInterface $objectMapperMetadata = null)
54
    {
UNCOV
55
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
852✔
56
    }
57

58
    /**
59
     * Collects tags from the previous and the current version of the updated entities to purge related documents.
60
     */
61
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
62
    {
UNCOV
63
        $object = $eventArgs->getObject();
63✔
UNCOV
64
        $this->gatherResourceAndItemTags($object, true);
63✔
65

UNCOV
66
        $changeSet = $eventArgs->getEntityChangeSet();
63✔
67
        // @phpstan-ignore-next-line
UNCOV
68
        $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
63✔
UNCOV
69
        $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings();
63✔
70

UNCOV
71
        foreach ($changeSet as $key => $value) {
63✔
UNCOV
72
            if (!isset($associationMappings[$key])) {
62✔
UNCOV
73
                continue;
47✔
74
            }
75

76
            $this->addTagsFor($value[0]);
18✔
77
            $this->addTagsFor($value[1]);
18✔
78
        }
79
    }
80

81
    /**
82
     * Collects tags from updated and deleted entities, including relations.
83
     */
84
    public function onFlush(OnFlushEventArgs $eventArgs): void
85
    {
86
        // @phpstan-ignore-next-line
UNCOV
87
        $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
852✔
UNCOV
88
        $uow = $em->getUnitOfWork();
852✔
89

UNCOV
90
        foreach ($this->scheduledInsertions = $uow->getScheduledEntityInsertions() as $entity) {
852✔
91
            // inserts shouldn't add new related entities, we should be able to gather related tags already
UNCOV
92
            $this->gatherRelationTags($em, $entity);
802✔
93
        }
94

UNCOV
95
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
852✔
UNCOV
96
            $this->gatherResourceAndItemTags($entity, true);
63✔
UNCOV
97
            $this->gatherRelationTags($em, $entity);
63✔
98
        }
99

UNCOV
100
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
852✔
UNCOV
101
            $this->gatherResourceAndItemTags($entity, true);
18✔
UNCOV
102
            $this->gatherRelationTags($em, $entity);
18✔
103
        }
104
    }
105

106
    /**
107
     * Purges tags collected during this request, and clears the tag list.
108
     */
109
    public function postFlush(): void
110
    {
111
        // since IRIs can't always be generated for new entities (missing auto-generated IDs), we need to gather the related IRIs after flush()
UNCOV
112
        foreach ($this->scheduledInsertions as $entity) {
852✔
UNCOV
113
            $this->gatherResourceAndItemTags($entity, false);
802✔
114
        }
115

UNCOV
116
        if (empty($this->tags)) {
852✔
UNCOV
117
            return;
35✔
118
        }
119

UNCOV
120
        $this->purger->purge(array_values($this->tags));
823✔
121

UNCOV
122
        $this->tags = [];
823✔
123
    }
124

125
    private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void
126
    {
UNCOV
127
        $resources = $this->getResourcesForEntity($entity);
846✔
128

UNCOV
129
        foreach ($resources as $resource) {
846✔
130
            try {
UNCOV
131
                $iri = $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, new GetCollection());
823✔
UNCOV
132
                $this->tags[$iri] = $iri;
823✔
133

UNCOV
134
                if ($purgeItem) {
823✔
UNCOV
135
                    $this->addTagForItem($entity);
823✔
136
                }
137
            } catch (OperationNotFoundException|InvalidArgumentException) {
×
138
            }
139
        }
140
    }
141

142
    private function gatherRelationTags(EntityManagerInterface $em, object $entity): void
143
    {
UNCOV
144
        $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings();
846✔
145

146
        /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */
UNCOV
147
        foreach ($associationMappings as $property => $associationMapping) {
846✔
UNCOV
148
            if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) {
401✔
UNCOV
149
                return;
41✔
150
            }
UNCOV
151
            if (!$this->propertyAccessor->isReadable($entity, $property)) {
376✔
UNCOV
152
                return;
13✔
153
            }
154

155
            if (
UNCOV
156
                \is_array($associationMapping)
373✔
UNCOV
157
                && \array_key_exists('targetEntity', $associationMapping)
373✔
UNCOV
158
                && ($targetEntity = $associationMapping['targetEntity'])
373✔
UNCOV
159
                && !$this->resourceClassResolver->isResourceClass($targetEntity)
373✔
160
            ) {
161
                if (!$this->objectMapper) {
×
162
                    return;
×
163
                }
164

165
                $targetRefl = new \ReflectionClass($targetEntity);
×
166
                if ($this->objectMapperMetadata) {
×
167
                    if (!$this->objectMapperMetadata->create($targetRefl->newInstanceWithoutConstructor())) {
×
168
                        return;
×
169
                    }
170
                } elseif (!$targetRefl->getAttributes(Map::class)) {
×
171
                    return;
×
172
                }
173
            }
174

UNCOV
175
            $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
373✔
176
        }
177
    }
178

179
    private function addTagsFor(mixed $value): void
180
    {
UNCOV
181
        if (!$value || \is_scalar($value)) {
373✔
UNCOV
182
            return;
211✔
183
        }
184

UNCOV
185
        if (!is_iterable($value)) {
359✔
UNCOV
186
            $this->addTagForItem($value);
240✔
187

UNCOV
188
            return;
240✔
189
        }
190

UNCOV
191
        if ($value instanceof PersistentCollection) {
271✔
UNCOV
192
            $value = clone $value;
271✔
193
        }
194

UNCOV
195
        foreach ($value as $v) {
271✔
UNCOV
196
            $this->addTagForItem($v);
108✔
197
        }
198
    }
199

200
    private function addTagForItem(mixed $value): void
201
    {
UNCOV
202
        $resources = $this->getResourcesForEntity($value);
300✔
203

UNCOV
204
        foreach ($resources as $resource) {
300✔
205
            try {
UNCOV
206
                $iri = $this->iriConverter->getIriFromResource($resource);
300✔
UNCOV
207
                $this->tags[$iri] = $iri;
149✔
UNCOV
208
            } catch (OperationNotFoundException|InvalidArgumentException) {
164✔
209
            }
210
        }
211
    }
212

213
    private function getResourcesForEntity(object $entity): array
214
    {
UNCOV
215
        $resources = [];
846✔
216

UNCOV
217
        if (!$this->resourceClassResolver->isResourceClass($class = $this->getObjectClass($entity))) {
846✔
218
            // is the entity mapped to resource(s)?
UNCOV
219
            if (!$this->objectMapper) {
40✔
220
                return [];
×
221
            }
222

UNCOV
223
            if ($this->objectMapperMetadata) {
40✔
UNCOV
224
                $mappings = $this->objectMapperMetadata->create($entity);
40✔
225

UNCOV
226
                if (!$mappings) {
40✔
UNCOV
227
                    return [];
32✔
228
                }
229

UNCOV
230
                $resources = array_map(
8✔
UNCOV
231
                    fn ($mapping) => $this->objectMapper->map($entity, $mapping->target),
8✔
UNCOV
232
                    $mappings
8✔
UNCOV
233
                );
8✔
234
            } else {
235
                $mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);
×
236

237
                if (!$mapAttributes) {
×
238
                    return [];
×
239
                }
240

241
                // loop over all mappings to fetch all resources mapped to this entity
242
                $resources = array_map(
×
243
                    fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
×
244
                    $mapAttributes
×
245
                );
×
246
            }
247
        } else {
UNCOV
248
            $resources[] = $entity;
815✔
249
        }
250

UNCOV
251
        return $resources;
823✔
252
    }
253
}
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

© 2026 Coveralls, Inc