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

api-platform / core / 18223414080

03 Oct 2025 01:18PM UTC coverage: 0.0% (-22.0%) from 21.956%
18223414080

Pull #7397

github

web-flow
Merge 69d085182 into 0b8237918
Pull Request #7397: fix(jsonschema/jsonld): make `@id` and `@type` properties required only in the JSON-LD schema for output

0 of 18 new or added lines in 2 files covered. (0.0%)

12304 existing lines in 405 files now uncovered.

0 of 53965 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\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
    private array $scheduledInsertions = [];
46

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

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

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

UNCOV
69
        foreach ($changeSet as $key => $value) {
×
UNCOV
70
            if (!isset($associationMappings[$key])) {
×
UNCOV
71
                continue;
×
72
            }
73

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

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

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

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

UNCOV
98
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
×
99
            $this->gatherResourceAndItemTags($entity, true);
×
100
            $this->gatherRelationTags($em, $entity);
×
101
        }
102
    }
103

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

UNCOV
114
        if (empty($this->tags)) {
×
UNCOV
115
            return;
×
116
        }
117

UNCOV
118
        $this->purger->purge(array_values($this->tags));
×
119

UNCOV
120
        $this->tags = [];
×
121
    }
122

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

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

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

140
    private function gatherRelationTags(EntityManagerInterface $em, object $entity): void
141
    {
UNCOV
142
        $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings();
×
143

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

153
            if (
UNCOV
154
                \is_array($associationMapping)
×
UNCOV
155
                && \array_key_exists('targetEntity', $associationMapping)
×
UNCOV
156
                && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])
×
157
                && (
UNCOV
158
                    !$this->objectMapper
×
UNCOV
159
                    || !(new \ReflectionClass($associationMapping['targetEntity']))->getAttributes(Map::class)
×
160
                )
161
            ) {
162
                return;
×
163
            }
164

UNCOV
165
            $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
×
166
        }
167
    }
168

169
    private function addTagsFor(mixed $value): void
170
    {
UNCOV
171
        if (!$value || \is_scalar($value)) {
×
UNCOV
172
            return;
×
173
        }
174

UNCOV
175
        if (!is_iterable($value)) {
×
UNCOV
176
            $this->addTagForItem($value);
×
177

UNCOV
178
            return;
×
179
        }
180

UNCOV
181
        if ($value instanceof PersistentCollection) {
×
UNCOV
182
            $value = clone $value;
×
183
        }
184

UNCOV
185
        foreach ($value as $v) {
×
UNCOV
186
            $this->addTagForItem($v);
×
187
        }
188
    }
189

190
    private function addTagForItem(mixed $value): void
191
    {
UNCOV
192
        $resources = $this->getResourcesForEntity($value);
×
193

UNCOV
194
        foreach ($resources as $resource) {
×
195
            try {
UNCOV
196
                $iri = $this->iriConverter->getIriFromResource($resource);
×
UNCOV
197
                $this->tags[$iri] = $iri;
×
UNCOV
198
            } catch (OperationNotFoundException|InvalidArgumentException) {
×
199
            }
200
        }
201
    }
202

203
    private function getResourcesForEntity(object $entity): array
204
    {
UNCOV
205
        $resources = [];
×
206

UNCOV
207
        if (!$this->resourceClassResolver->isResourceClass($class = $this->getObjectClass($entity))) {
×
208
            // is the entity mapped to resource(s)?
UNCOV
209
            if (!$this->objectMapper) {
×
210
                return [];
×
211
            }
212

UNCOV
213
            $mapAttributes = (new \ReflectionClass($class))->getAttributes(Map::class);
×
214

UNCOV
215
            if (!$mapAttributes) {
×
UNCOV
216
                return [];
×
217
            }
218

219
            // loop over all mappings to fetch all resources mapped to this entity
UNCOV
220
            $resources = array_map(
×
UNCOV
221
                fn ($mapAttribute) => $this->objectMapper->map($entity, $mapAttribute->newInstance()->target),
×
UNCOV
222
                $mapAttributes
×
UNCOV
223
            );
×
224
        } else {
UNCOV
225
            $resources[] = $entity;
×
226
        }
227

UNCOV
228
        return $resources;
×
229
    }
230
}
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