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

api-platform / core / 19337721455

13 Nov 2025 04:02PM UTC coverage: 0.0% (-24.6%) from 24.631%
19337721455

push

github

soyuka
Merge 4.1

0 of 56854 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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();
×
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();
×
64
        $this->gatherResourceAndItemTags($object, true);
×
65

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

71
        foreach ($changeSet as $key => $value) {
×
72
            if (!isset($associationMappings[$key])) {
×
73
                continue;
×
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();
×
88
        $uow = $em->getUnitOfWork();
×
89

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

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

100
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
×
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) {
×
113
            $this->gatherResourceAndItemTags($entity, false);
×
114
        }
115

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

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

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

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

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

134
                if ($purgeItem) {
×
135
                    $this->addTagForItem($entity);
×
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();
×
145

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

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

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

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

188
            return;
×
189
        }
190

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

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

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

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

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

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

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

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

230
                $resources = array_map(
×
231
                    fn ($mapping) => $this->objectMapper->map($entity, $mapping->target),
×
232
                    $mappings
×
233
                );
×
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;
×
249
        }
250

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