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

api-platform / core / 16050929464

03 Jul 2025 12:51PM UTC coverage: 22.065% (+0.2%) from 21.821%
16050929464

push

github

soyuka
chore: todo improvement

11516 of 52192 relevant lines covered (22.06%)

22.08 hits per line

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

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

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

44
    public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null)
45
    {
46
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
250✔
47
    }
48

49
    /**
50
     * Collects tags from the previous and the current version of the updated entities to purge related documents.
51
     */
52
    public function preUpdate(PreUpdateEventArgs $eventArgs): void
53
    {
54
        $object = $eventArgs->getObject();
×
55
        $this->gatherResourceAndItemTags($object, true);
×
56

57
        $changeSet = $eventArgs->getEntityChangeSet();
×
58
        // @phpstan-ignore-next-line
59
        $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
×
60
        $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings();
×
61

62
        foreach ($changeSet as $key => $value) {
×
63
            if (!isset($associationMappings[$key])) {
×
64
                continue;
×
65
            }
66

67
            $this->addTagsFor($value[0]);
×
68
            $this->addTagsFor($value[1]);
×
69
        }
70
    }
71

72
    /**
73
     * Collects tags from inserted and deleted entities, including relations.
74
     */
75
    public function onFlush(OnFlushEventArgs $eventArgs): void
76
    {
77
        // @phpstan-ignore-next-line
78
        $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager();
250✔
79
        $uow = $em->getUnitOfWork();
250✔
80

81
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
250✔
82
            $this->gatherResourceAndItemTags($entity, false);
250✔
83
            $this->gatherRelationTags($em, $entity);
250✔
84
        }
85

86
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
250✔
87
            $this->gatherResourceAndItemTags($entity, true);
×
88
            $this->gatherRelationTags($em, $entity);
×
89
        }
90

91
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
250✔
92
            $this->gatherResourceAndItemTags($entity, true);
×
93
            $this->gatherRelationTags($em, $entity);
×
94
        }
95
    }
96

97
    /**
98
     * Purges tags collected during this request, and clears the tag list.
99
     */
100
    public function postFlush(): void
101
    {
102
        if (empty($this->tags)) {
250✔
103
            return;
×
104
        }
105

106
        $this->purger->purge(array_values($this->tags));
250✔
107

108
        $this->tags = [];
250✔
109
    }
110

111
    private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void
112
    {
113
        try {
114
            $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, new GetCollection());
250✔
115
            $this->tags[$iri] = $iri;
250✔
116

117
            if ($purgeItem) {
250✔
118
                $this->addTagForItem($entity);
250✔
119
            }
120
        } catch (OperationNotFoundException|InvalidArgumentException) {
4✔
121
        }
122
    }
123

124
    private function gatherRelationTags(EntityManagerInterface $em, object $entity): void
125
    {
126
        $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings();
250✔
127
        /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */
128
        foreach ($associationMappings as $property => $associationMapping) {
250✔
129
            if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) {
46✔
130
                return;
22✔
131
            }
132

133
            if (
134
                \is_array($associationMapping)
30✔
135
                && \array_key_exists('targetEntity', $associationMapping)
30✔
136
                && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) {
30✔
137
                return;
×
138
            }
139

140
            if ($this->propertyAccessor->isReadable($entity, $property)) {
30✔
141
                $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
30✔
142
            }
143
        }
144
    }
145

146
    private function addTagsFor(mixed $value): void
147
    {
148
        if (!$value || \is_scalar($value)) {
30✔
149
            return;
6✔
150
        }
151

152
        if (!is_iterable($value)) {
30✔
153
            $this->addTagForItem($value);
18✔
154

155
            return;
18✔
156
        }
157

158
        if ($value instanceof PersistentCollection) {
16✔
159
            $value = clone $value;
16✔
160
        }
161

162
        foreach ($value as $v) {
16✔
163
            $this->addTagForItem($v);
4✔
164
        }
165
    }
166

167
    private function addTagForItem(mixed $value): void
168
    {
169
        if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) {
18✔
170
            return;
×
171
        }
172

173
        try {
174
            $iri = $this->iriConverter->getIriFromResource($value);
18✔
175
            $this->tags[$iri] = $iri;
2✔
176
        } catch (RuntimeException|InvalidArgumentException) {
16✔
177
        }
178
    }
179
}
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