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

api-platform / core / 16598414478

29 Jul 2025 02:03PM UTC coverage: 22.182% (+0.2%) from 21.944%
16598414478

Pull #7319

github

web-flow
Merge 914d992b2 into d06b1a0a0
Pull Request #7319: feat(doctrine): improve http cache invalidation using the mapping

24 of 58 new or added lines in 3 files covered. (41.38%)

2 existing lines in 1 file now uncovered.

11645 of 52498 relevant lines covered (22.18%)

23.66 hits per line

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

0.0
/src/Symfony/Tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.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\Tests\Doctrine\EventListener;
15

16
use ApiPlatform\HttpCache\PurgerInterface;
17
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
18
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
19
use ApiPlatform\Metadata\GetCollection;
20
use ApiPlatform\Metadata\IriConverterInterface;
21
use ApiPlatform\Metadata\ResourceClassResolverInterface;
22
use ApiPlatform\Metadata\UrlGeneratorInterface;
23
use ApiPlatform\Symfony\Doctrine\EventListener\PurgeHttpCacheListener;
24
use ApiPlatform\Symfony\Tests\Fixtures\MappedEntity;
25
use ApiPlatform\Symfony\Tests\Fixtures\NotAResource;
26
use ApiPlatform\Symfony\Tests\Fixtures\TestBundle\Entity\ContainNonResource;
27
use ApiPlatform\Symfony\Tests\Fixtures\TestBundle\Entity\Dummy;
28
use ApiPlatform\Symfony\Tests\Fixtures\TestBundle\Entity\DummyNoGetOperation;
29
use ApiPlatform\Symfony\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
30
use Doctrine\ORM\EntityManagerInterface;
31
use Doctrine\ORM\Event\OnFlushEventArgs;
32
use Doctrine\ORM\Event\PreUpdateEventArgs;
33
use Doctrine\ORM\Mapping\ClassMetadata;
34
use Doctrine\ORM\UnitOfWork;
35
use PHPUnit\Framework\TestCase;
36
use Prophecy\Argument;
37
use Prophecy\PhpUnit\ProphecyTrait;
38
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
39

40
/**
41
 * @author Kévin Dunglas <dunglas@gmail.com>
42
 */
43
class PurgeHttpCacheListenerTest extends TestCase
44
{
45
    use ProphecyTrait;
46

47
    public function testOnFlush(): void
48
    {
49
        $toInsert1 = new Dummy();
×
50
        $toInsert2 = new Dummy();
×
51

52
        $toUpdate1 = new Dummy();
×
53
        $toUpdate1->setId(1);
×
54
        $toUpdate2 = new Dummy();
×
55
        $toUpdate2->setId(2);
×
56

57
        $toDelete1 = new Dummy();
×
58
        $toDelete1->setId(3);
×
59
        $toDelete2 = new Dummy();
×
60
        $toDelete2->setId(4);
×
61

62
        $toDeleteNoPurge = new DummyNoGetOperation();
×
63
        $toDeleteNoPurge->setId(5);
×
64

65
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
66
        $purgerProphecy->purge(['/dummies', '/dummies/1', '/dummies/2', '/dummies/3', '/dummies/4'])->shouldBeCalled();
×
67

68
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
69
        $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled();
×
70
        $iriConverterProphecy->getIriFromResource($toUpdate1)->willReturn('/dummies/1')->shouldBeCalled();
×
71
        $iriConverterProphecy->getIriFromResource($toUpdate2)->willReturn('/dummies/2')->shouldBeCalled();
×
72
        $iriConverterProphecy->getIriFromResource($toDelete1)->willReturn('/dummies/3')->shouldBeCalled();
×
73
        $iriConverterProphecy->getIriFromResource($toDelete2)->willReturn('/dummies/4')->shouldBeCalled();
×
74
        $iriConverterProphecy->getIriFromResource(Argument::type(DummyNoGetOperation::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled();
×
75
        $iriConverterProphecy->getIriFromResource(Argument::any())->willThrow(new ItemNotFoundException());
×
76

77
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
78
        $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled();
×
79

80
        $uowProphecy = $this->prophesize(UnitOfWork::class);
×
81
        $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1, $toInsert2])->shouldBeCalled();
×
82
        $uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate1, $toUpdate2])->shouldBeCalled();
×
83
        $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge])->shouldBeCalled();
×
84

85
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
86
        $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
×
87
        $dummyClassMetadata = new ClassMetadata(Dummy::class);
×
88
        // @phpstan-ignore-next-line
89
        $dummyClassMetadata->associationMappings = [
×
90
            'relatedDummy' => [],
×
91
            'relatedOwningDummy' => [],
×
92
        ];
×
93
        $emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata)->shouldBeCalled();
×
94
        $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn(new ClassMetadata(DummyNoGetOperation::class))->shouldBeCalled();
×
95
        $eventArgs = new OnFlushEventArgs($emProphecy->reveal());
×
96

97
        $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
×
98
        $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true);
×
99
        $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false);
×
100
        $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null)->shouldBeCalled();
×
101
        $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null)->shouldNotBeCalled();
×
102

103
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal());
×
104
        $listener->onFlush($eventArgs);
×
105
        $listener->postFlush();
×
106

107
        $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->shouldHaveBeenCalled();
×
108
        $iriConverterProphecy->getIriFromResource(Argument::type(DummyNoGetOperation::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->shouldHaveBeenCalled();
×
109
    }
110

111
    public function testPreUpdate(): void
112
    {
113
        $oldRelatedDummy = new RelatedDummy();
×
114
        $oldRelatedDummy->setId(1);
×
115

116
        $newRelatedDummy = new RelatedDummy();
×
117
        $newRelatedDummy->setId(2);
×
118

119
        $dummy = new Dummy();
×
120
        $dummy->setId(1);
×
121

122
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
123
        $purgerProphecy->purge(['/dummies', '/dummies/1', '/related_dummies/old', '/related_dummies/new'])->shouldBeCalled();
×
124

125
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
126
        $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled();
×
127
        $iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldBeCalled();
×
128
        $iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled();
×
129
        $iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled();
×
130

131
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
132
        $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled();
×
133

134
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
135

136
        $classMetadata = new ClassMetadata(Dummy::class);
×
137
        $classMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class]);
×
138
        $emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadata)->shouldBeCalled();
×
139

140
        $changeSet = ['relatedDummy' => [$oldRelatedDummy, $newRelatedDummy]];
×
141
        $eventArgs = new PreUpdateEventArgs($dummy, $emProphecy->reveal(), $changeSet);
×
142

143
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal());
×
144
        $listener->preUpdate($eventArgs);
×
145
        $listener->postFlush();
×
146
    }
147

148
    public function testNothingToPurge(): void
149
    {
150
        $dummyNoGetOperation = new DummyNoGetOperation();
×
151
        $dummyNoGetOperation->setId(1);
×
152

153
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
154
        $purgerProphecy->purge([])->shouldNotBeCalled();
×
155

156
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
157
        $iriConverterProphecy->getIriFromResource(Argument::type(DummyNoGetOperation::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled();
×
158
        $iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->shouldNotBeCalled();
×
159

160
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
161
        $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldNotBeCalled();
×
162

163
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
164

165
        $classMetadata = new ClassMetadata(DummyNoGetOperation::class);
×
166
        $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn($classMetadata)->shouldBeCalled();
×
167

168
        $changeSet = ['lorem' => ['ipsum1', 'ipsum2']];
×
169
        $eventArgs = new PreUpdateEventArgs($dummyNoGetOperation, $emProphecy->reveal(), $changeSet);
×
170

171
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal());
×
172
        $listener->preUpdate($eventArgs);
×
173
        $listener->postFlush();
×
174
    }
175

176
    public function testNotAResourceClass(): void
177
    {
178
        $containNonResource = new ContainNonResource();
×
179
        $nonResource1 = new NotAResource('foo', 'bar');
×
180
        $nonResource2 = new NotAResource('baz', 'qux');
×
181
        $collectionOfNotAResource = [$nonResource1, $nonResource2];
×
182

183
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
184
        $purgerProphecy->purge(['/dummies'])->shouldBeCalled();
×
185

186
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
187
        $iriConverterProphecy->getIriFromResource(Argument::type(ContainNonResource::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled();
×
188
        $iriConverterProphecy->getIriFromResource($nonResource1)->willThrow(new InvalidArgumentException())->shouldBeCalled();
×
189
        $iriConverterProphecy->getIriFromResource($nonResource2)->willThrow(new InvalidArgumentException())->shouldBeCalled();
×
190

191
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
192
        $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled();
×
193

194
        $uowProphecy = $this->prophesize(UnitOfWork::class);
×
195
        $uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled();
×
196
        $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled();
×
197
        $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled();
×
198

199
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
200
        $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
×
201

202
        $dummyClassMetadata = new ClassMetadata(ContainNonResource::class);
×
203
        // @phpstan-ignore-next-line
204
        $dummyClassMetadata->associationMappings = [
×
205
            'notAResource' => [],
×
206
            'collectionOfNotAResource' => ['targetEntity' => NotAResource::class],
×
207
        ];
×
208
        $emProphecy->getClassMetadata(ContainNonResource::class)->willReturn($dummyClassMetadata);
×
209
        $eventArgs = new OnFlushEventArgs($emProphecy->reveal());
×
210

211
        $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
×
212
        $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true);
×
213
        $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->willReturn(true);
×
214
        $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldBeCalled()->willReturn($nonResource1);
×
215
        $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldBeCalled()->willReturn($collectionOfNotAResource);
×
216

217
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal());
×
218
        $listener->onFlush($eventArgs);
×
219
        $listener->postFlush();
×
220
    }
221

222
    public function testAddTagsForCollection(): void
223
    {
224
        $dummy1 = new Dummy();
×
225
        $dummy1->setId(1);
×
226
        $dummy2 = new Dummy();
×
227
        $dummy2->setId(2);
×
228
        $collection = [$dummy1, $dummy2];
×
229

230
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
231
        $purgerProphecy->purge(['/dummies', '/dummies/1', '/dummies/2'])->shouldBeCalled();
×
232

233
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
234
        $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled();
×
235
        $iriConverterProphecy->getIriFromResource($dummy1)->willReturn('/dummies/1')->shouldBeCalled();
×
236
        $iriConverterProphecy->getIriFromResource($dummy2)->willReturn('/dummies/2')->shouldBeCalled();
×
237

238
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
239
        $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled();
×
240

241
        $dummyWithCollection = new Dummy();
×
242
        $dummyWithCollection->setId(3);
×
243

244
        $uowProphecy = $this->prophesize(UnitOfWork::class);
×
245
        $uowProphecy->getScheduledEntityInsertions()->willReturn([$dummyWithCollection])->shouldBeCalled();
×
246
        $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled();
×
247
        $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled();
×
248

249
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
250
        $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
×
251

252
        $dummyClassMetadata = new ClassMetadata(Dummy::class);
×
253
        // @phpstan-ignore-next-line
254
        $dummyClassMetadata->associationMappings = [
×
255
            'relatedDummies' => ['targetEntity' => Dummy::class],
×
256
        ];
×
257
        $emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata)->shouldBeCalled();
×
258
        $eventArgs = new OnFlushEventArgs($emProphecy->reveal());
×
259

260
        $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
×
261
        $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummies')->willReturn(true);
×
262
        $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummies')->willReturn($collection)->shouldBeCalled();
×
263

264
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal());
×
265
        $listener->onFlush($eventArgs);
×
266
        $listener->postFlush();
×
267
    }
268

269
    public function testMappedResources(): void
270
    {
NEW
271
        $mappedEntity = new MappedEntity();
×
272

NEW
273
        $purgerProphecy = $this->prophesize(PurgerInterface::class);
×
NEW
274
        $purgerProphecy->purge(['/mapped_ressources'])->shouldBeCalled();
×
275

NEW
276
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
NEW
277
        $iriConverterProphecy->getIriFromResource(Argument::type(MappedEntity::class), UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/mapped_ressources')->shouldBeCalled();
×
278

NEW
279
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
NEW
280
        $resourceClassResolverProphecy->isResourceClass(MappedEntity::class)->willReturn(true)->shouldBeCalled();
×
281

NEW
282
        $uowProphecy = $this->prophesize(UnitOfWork::class);
×
NEW
283
        $uowProphecy->getScheduledEntityInsertions()->willReturn([$mappedEntity])->shouldBeCalled();
×
NEW
284
        $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled();
×
NEW
285
        $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled();
×
286

NEW
287
        $emProphecy = $this->prophesize(EntityManagerInterface::class);
×
NEW
288
        $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled();
×
NEW
289
        $classMetadata = new ClassMetadata(MappedEntity::class);
×
290
        // @phpstan-ignore-next-line
NEW
291
        $classMetadata->associationMappings = [];
×
NEW
292
        $emProphecy->getClassMetadata(MappedEntity::class)->willReturn($classMetadata)->shouldBeCalled();
×
NEW
293
        $eventArgs = new OnFlushEventArgs($emProphecy->reveal());
×
294

NEW
295
        $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
×
296

NEW
297
        $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal());
×
NEW
298
        $listener->onFlush($eventArgs);
×
NEW
299
        $listener->postFlush();
×
300
    }
301
}
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