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

api-platform / core / 9710836697

28 Jun 2024 09:35AM UTC coverage: 63.285% (+1.2%) from 62.122%
9710836697

push

github

soyuka
docs: changelog v3.3.7

11104 of 17546 relevant lines covered (63.29%)

52.26 hits per line

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

96.43
/src/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\Doctrine\EventListener;
15

16
use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface;
17
use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
18
use ApiPlatform\Exception\InvalidArgumentException;
19
use ApiPlatform\Exception\OperationNotFoundException;
20
use ApiPlatform\Exception\RuntimeException;
21
use ApiPlatform\HttpCache\PurgerInterface;
22
use ApiPlatform\Metadata\GetCollection;
23
use ApiPlatform\Metadata\IriConverterInterface;
24
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25
use ApiPlatform\Metadata\UrlGeneratorInterface;
26
use ApiPlatform\Metadata\Util\ClassInfoTrait;
27
use Doctrine\ORM\EntityManagerInterface;
28
use Doctrine\ORM\Event\OnFlushEventArgs;
29
use Doctrine\ORM\Event\PreUpdateEventArgs;
30
use Doctrine\ORM\Mapping\AssociationMapping;
31
use Doctrine\ORM\PersistentCollection;
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
    public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null)
47
    {
48
        $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
87✔
49
    }
50

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

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

64
        foreach ($changeSet as $key => $value) {
8✔
65
            if (!isset($associationMappings[$key])) {
8✔
66
                continue;
4✔
67
            }
68

69
            $this->addTagsFor($value[0]);
4✔
70
            $this->addTagsFor($value[1]);
4✔
71
        }
72
    }
73

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

83
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
79✔
84
            $this->gatherResourceAndItemTags($entity, false);
79✔
85
            $this->gatherRelationTags($em, $entity);
79✔
86
        }
87

88
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
79✔
89
            $this->gatherResourceAndItemTags($entity, true);
4✔
90
            $this->gatherRelationTags($em, $entity);
4✔
91
        }
92

93
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
79✔
94
            $this->gatherResourceAndItemTags($entity, true);
4✔
95
            $this->gatherRelationTags($em, $entity);
4✔
96
        }
97
    }
98

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

108
        $this->purger->purge(array_values($this->tags));
79✔
109

110
        $this->tags = [];
79✔
111
    }
112

113
    private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void
114
    {
115
        try {
116
            $resourceClass = $this->resourceClassResolver->getResourceClass($entity);
87✔
117
            $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection());
87✔
118
            $this->tags[$iri] = $iri;
83✔
119

120
            if ($purgeItem) {
83✔
121
                $this->addTagForItem($entity);
83✔
122
            }
123
        } catch (OperationNotFoundException|InvalidArgumentException) {
8✔
124
        }
125
    }
126

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

136
            if (
137
                \is_array($associationMapping)
44✔
138
                && \array_key_exists('targetEntity', $associationMapping)
44✔
139
                && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) {
44✔
140
                return;
4✔
141
            }
142

143
            if ($this->propertyAccessor->isReadable($entity, $property)) {
44✔
144
                $this->addTagsFor($this->propertyAccessor->getValue($entity, $property));
44✔
145
            }
146
        }
147
    }
148

149
    private function addTagsFor(mixed $value): void
150
    {
151
        if (!$value || \is_scalar($value)) {
48✔
152
            return;
16✔
153
        }
154

155
        if (!is_iterable($value)) {
44✔
156
            $this->addTagForItem($value);
20✔
157

158
            return;
20✔
159
        }
160

161
        if ($value instanceof PersistentCollection) {
24✔
162
            $value = clone $value;
24✔
163
        }
164

165
        foreach ($value as $v) {
24✔
166
            $this->addTagForItem($v);
×
167
        }
168
    }
169

170
    private function addTagForItem(mixed $value): void
171
    {
172
        if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) {
24✔
173
            return;
4✔
174
        }
175

176
        try {
177
            $iri = $this->iriConverter->getIriFromResource($value);
20✔
178
            $this->tags[$iri] = $iri;
8✔
179
        } catch (RuntimeException|InvalidArgumentException) {
12✔
180
        }
181
    }
182
}
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