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

api-platform / core / 19250029811

10 Nov 2025 11:56PM UTC coverage: 24.566% (+24.6%) from 0.0%
19250029811

push

github

soyuka
ci: ignore no-unnecessary-combinator

14095 of 57375 relevant lines covered (24.57%)

26.48 hits per line

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

76.14
/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
    {
55
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
310✔
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
    {
63
        $object = $eventArgs->getObject();
2✔
64
        $this->gatherResourceAndItemTags($object, true);
2✔
65

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

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

76
            $this->addTagsFor($value[0]);
×
77
            $this->addTagsFor($value[1]);
×
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
87
        $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
310✔
88
        $uow = $em->getUnitOfWork();
310✔
89

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

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

100
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
310✔
101
            $this->gatherResourceAndItemTags($entity, true);
×
102
            $this->gatherRelationTags($em, $entity);
×
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()
112
        foreach ($this->scheduledInsertions as $entity) {
310✔
113
            $this->gatherResourceAndItemTags($entity, false);
310✔
114
        }
115

116
        if (empty($this->tags)) {
310✔
117
            return;
10✔
118
        }
119

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

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

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

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

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

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

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

155
            if (
156
                \is_array($associationMapping)
70✔
157
                && \array_key_exists('targetEntity', $associationMapping)
70✔
158
                && ($targetEntity = $associationMapping['targetEntity'])
70✔
159
                && !$this->resourceClassResolver->isResourceClass($targetEntity)
70✔
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

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

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

185
        if (!is_iterable($value)) {
70✔
186
            $this->addTagForItem($value);
56✔
187

188
            return;
56✔
189
        }
190

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

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

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

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

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

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

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

226
                if (!$mappings) {
18✔
227
                    return [];
10✔
228
                }
229

230
                $resources = array_map(
8✔
231
                    fn ($mapping) => $this->objectMapper->map($entity, $mapping->target),
8✔
232
                    $mappings
8✔
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 {
248
            $resources[] = $entity;
294✔
249
        }
250

251
        return $resources;
302✔
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

© 2025 Coveralls, Inc