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

api-platform / core / 17063025732

19 Aug 2025 07:44AM UTC coverage: 22.236% (+0.05%) from 22.188%
17063025732

push

github

soyuka
test: skip mongodb bundle when extension is not loaded

11683 of 52542 relevant lines covered (22.24%)

24.04 hits per line

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

83.56
/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\ObjectMapperInterface;
31
use Symfony\Component\PropertyAccess\PropertyAccess;
32
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
33

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

45
    public function __construct(private readonly PurgerInterface $purger,
46
        private readonly IriConverterInterface $iriConverter,
47
        private readonly ResourceClassResolverInterface $resourceClassResolver,
48
        ?PropertyAccessorInterface $propertyAccessor = null,
49
        private readonly ?ObjectMapperInterface $objectMapper = null)
50
    {
51
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
256✔
52
    }
53

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

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

67
        foreach ($changeSet as $key => $value) {
2✔
68
            if (!isset($associationMappings[$key])) {
2✔
69
                continue;
2✔
70
            }
71

72
            $this->addTagsFor($value[0]);
×
73
            $this->addTagsFor($value[1]);
×
74
        }
75
    }
76

77
    /**
78
     * Collects tags from inserted and deleted entities, including relations.
79
     */
80
    public function onFlush(OnFlushEventArgs $eventArgs): void
81
    {
82
        // @phpstan-ignore-next-line
83
        $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
256✔
84
        $uow = $em->getUnitOfWork();
256✔
85

86
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
256✔
87
            $this->gatherResourceAndItemTags($entity, false);
256✔
88
            $this->gatherRelationTags($em, $entity);
256✔
89
        }
90

91
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
256✔
92
            $this->gatherResourceAndItemTags($entity, true);
2✔
93
            $this->gatherRelationTags($em, $entity);
2✔
94
        }
95

96
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
256✔
97
            $this->gatherResourceAndItemTags($entity, true);
×
98
            $this->gatherRelationTags($em, $entity);
×
99
        }
100
    }
101

102
    /**
103
     * Purges tags collected during this request, and clears the tag list.
104
     */
105
    public function postFlush(): void
106
    {
107
        if (empty($this->tags)) {
256✔
108
            return;
12✔
109
        }
110

111
        $this->purger->purge(array_values($this->tags));
244✔
112

113
        $this->tags = [];
244✔
114
    }
115

116
    private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void
117
    {
118
        $resources = $this->getResourcesForEntity($entity);
256✔
119

120
        foreach ($resources as $resource) {
256✔
121
            try {
122
                $iri = $this->iriConverter->getIriFromResource($resource, UrlGeneratorInterface::ABS_PATH, new GetCollection());
244✔
123
                $this->tags[$iri] = $iri;
244✔
124

125
                if ($purgeItem) {
244✔
126
                    $this->addTagForItem($entity);
244✔
127
                }
128
            } catch (OperationNotFoundException|InvalidArgumentException) {
4✔
129
            }
130
        }
131
    }
132

133
    private function gatherRelationTags(EntityManagerInterface $em, object $entity): void
134
    {
135
        $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings();
256✔
136

137
        /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */
138
        foreach ($associationMappings as $property => $associationMapping) {
256✔
139
            if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) {
48✔
140
                return;
24✔
141
            }
142
            if (!$this->propertyAccessor->isReadable($entity, $property)) {
30✔
143
                return;
6✔
144
            }
145

146
            if (
147
                \is_array($associationMapping)
30✔
148
                && \array_key_exists('targetEntity', $associationMapping)
30✔
149
                && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])
30✔
150
                && (
151
                    !$this->objectMapper
30✔
152
                    || !(new \ReflectionClass($associationMapping['targetEntity']))->getAttributes(Map::class)
30✔
153
                )
154
            ) {
155
                return;
×
156
            }
157

158
            $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
30✔
159
        }
160
    }
161

162
    private function addTagsFor(mixed $value): void
163
    {
164
        if (!$value || \is_scalar($value)) {
30✔
165
            return;
6✔
166
        }
167

168
        if (!is_iterable($value)) {
30✔
169
            $this->addTagForItem($value);
18✔
170

171
            return;
18✔
172
        }
173

174
        if ($value instanceof PersistentCollection) {
16✔
175
            $value = clone $value;
16✔
176
        }
177

178
        foreach ($value as $v) {
16✔
179
            $this->addTagForItem($v);
4✔
180
        }
181
    }
182

183
    private function addTagForItem(mixed $value): void
184
    {
185
        $resources = $this->getResourcesForEntity($value);
18✔
186

187
        foreach ($resources as $resource) {
18✔
188
            try {
189
                $iri = $this->iriConverter->getIriFromResource($resource);
18✔
190
                $this->tags[$iri] = $iri;
2✔
191
            } catch (OperationNotFoundException|InvalidArgumentException) {
16✔
192
            }
193
        }
194
    }
195

196
    private function getResourcesForEntity(object $entity): array
197
    {
198
        $resources = [];
256✔
199

200
        if (!$this->resourceClassResolver->isResourceClass($class = $this->getObjectClass($entity))) {
256✔
201
            // is the entity mapped to resource(s)?
202
            if (!$this->objectMapper) {
12✔
203
                return [];
12✔
204
            }
205

206
            $mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);
×
207

208
            if (!$mapAttributes) {
×
209
                return [];
×
210
            }
211

212
            // loop over all mappings to fetch all resources mapped to this entity
213
            $resources = array_map(
×
214
                fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
×
215
                $mapAttributes
×
216
            );
×
217
        } else {
218
            $resources[] = $entity;
244✔
219
        }
220

221
        return $resources;
244✔
222
    }
223
}
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