Skip to content

Commit b2c6f84

Browse files
authored
Merge pull request #1800 from soyuka/fix-identifier-cache
Fix identifier cache
2 parents 4e489cd + 76e7cef commit b2c6f84

File tree

7 files changed

+236
-95
lines changed

7 files changed

+236
-95
lines changed

src/Api/CachedIdentifiersExtractor.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,20 @@ final class CachedIdentifiersExtractor implements IdentifiersExtractorInterface
3333
private $cacheItemPool;
3434
private $propertyAccessor;
3535
private $decorated;
36+
private $resourceClassResolver;
3637
private $localCache = [];
3738
private $localResourceCache = [];
3839

39-
public function __construct(CacheItemPoolInterface $cacheItemPool, IdentifiersExtractorInterface $decorated, PropertyAccessorInterface $propertyAccessor = null)
40+
public function __construct(CacheItemPoolInterface $cacheItemPool, IdentifiersExtractorInterface $decorated, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null)
4041
{
4142
$this->cacheItemPool = $cacheItemPool;
4243
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
4344
$this->decorated = $decorated;
45+
$this->resourceClassResolver = $resourceClassResolver;
46+
47+
if (null === $this->resourceClassResolver) {
48+
@trigger_error(sprintf('Not injecting %s in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.', ResourceClassResolverInterface::class), E_USER_DEPRECATED);
49+
}
4450
}
4551

4652
/**
@@ -76,12 +82,12 @@ public function getIdentifiersFromItem($item): array
7682
continue;
7783
}
7884

79-
if (method_exists($identifiers[$propertyName], '__toString')) {
80-
$identifiers[$propertyName] = (string) $identifiers[$propertyName];
85+
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
86+
87+
if (null !== $this->resourceClassResolver && !$this->resourceClassResolver->isResourceClass($relatedResourceClass)) {
8188
continue;
8289
}
8390

84-
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
8591
if (!$relatedIdentifiers = $this->localCache[$relatedResourceClass] ?? false) {
8692
$relatedCacheKey = self::CACHE_KEY_PREFIX.md5($relatedResourceClass);
8793
try {

src/Api/IdentifiersExtractor.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ final class IdentifiersExtractor implements IdentifiersExtractorInterface
3232
private $propertyNameCollectionFactory;
3333
private $propertyMetadataFactory;
3434
private $propertyAccessor;
35+
private $resourceClassResolver;
3536

36-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null)
37+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null)
3738
{
3839
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
3940
$this->propertyMetadataFactory = $propertyMetadataFactory;
4041
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
42+
$this->resourceClassResolver = $resourceClassResolver;
43+
44+
if (null === $this->resourceClassResolver) {
45+
@trigger_error(sprintf('Not injecting %s in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.', ResourceClassResolverInterface::class), E_USER_DEPRECATED);
46+
}
4147
}
4248

4349
/**
@@ -68,14 +74,19 @@ public function getIdentifiersFromItem($item): array
6874
if (null === $identifier || false === $identifier) {
6975
continue;
7076
}
77+
7178
$identifier = $identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
79+
7280
if (!\is_object($identifier)) {
7381
continue;
74-
} elseif (method_exists($identifier, '__toString')) {
75-
$identifiers[$propertyName] = (string) $identifier;
76-
continue;
7782
}
83+
7884
$relatedResourceClass = $this->getObjectClass($identifier);
85+
86+
if (null !== $this->resourceClassResolver && !$this->resourceClassResolver->isResourceClass($relatedResourceClass)) {
87+
continue;
88+
}
89+
7990
$relatedItem = $identifier;
8091
unset($identifiers[$propertyName]);
8192
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {

src/Api/ResourceClassResolver.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ final class ResourceClassResolver implements ResourceClassResolverInterface
2828
use ClassInfoTrait;
2929

3030
private $resourceNameCollectionFactory;
31+
private $localIsResourceClassCache = [];
3132

3233
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory)
3334
{
@@ -69,12 +70,16 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
6970
*/
7071
public function isResourceClass(string $type): bool
7172
{
73+
if (isset($this->localIsResourceClassCache[$type])) {
74+
return $this->localIsResourceClassCache[$type];
75+
}
76+
7277
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
7378
if ($type === $resourceClass) {
74-
return true;
79+
return $this->localIsResourceClassCache[$type] = true;
7580
}
7681
}
7782

78-
return false;
83+
return $this->localIsResourceClassCache[$type] = false;
7984
}
8085
}

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,14 @@
217217
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
218218
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
219219
<argument type="service" id="api_platform.property_accessor" />
220+
<argument type="service" id="api_platform.resource_class_resolver" />
220221
</service>
221222

222223
<service id="api_platform.identifiers_extractor.cached" class="ApiPlatform\Core\Api\CachedIdentifiersExtractor" decorates="api_platform.identifiers_extractor" public="false">
223224
<argument type="service" id="api_platform.cache.identifiers_extractor" />
224225
<argument type="service" id="api_platform.identifiers_extractor.cached.inner" />
225226
<argument type="service" id="api_platform.property_accessor" />
227+
<argument type="service" id="api_platform.resource_class_resolver" />
226228
</service>
227229

228230
<!-- Subresources -->

tests/Api/CachedIdentifiersExtractorTest.php

Lines changed: 92 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515

1616
use ApiPlatform\Core\Api\CachedIdentifiersExtractor;
1717
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
18+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1819
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Generator\Uuid;
1920
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
2021
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
2122
use PHPUnit\Framework\TestCase;
23+
use Prophecy\Argument;
2224
use Psr\Cache\CacheItemInterface;
2325
use Psr\Cache\CacheItemPoolInterface;
2426

@@ -27,16 +29,21 @@
2729
*/
2830
class CachedIdentifiersExtractorTest extends TestCase
2931
{
30-
public function identifiersProvider()
32+
public function itemProvider()
3133
{
32-
yield [1, 1];
33-
yield [$uuid = new Uuid(), $uuid->__toString()];
34+
$dummy = new Dummy();
35+
$dummy->setId($id = 1);
36+
yield [$dummy, ['id' => $id]];
37+
38+
$dummy = new Dummy();
39+
$dummy->setId($id = new Uuid());
40+
yield [$dummy, ['id' => $id]];
3441
}
3542

3643
/**
37-
* @dataProvider identifiersProvider
44+
* @dataProvider itemProvider
3845
*/
39-
public function testFirstPass($identifier, $identifierValue)
46+
public function testFirstPass($item, $expected)
4047
{
4148
$key = 'iri_identifiers'.md5(Dummy::class);
4249

@@ -48,29 +55,25 @@ public function testFirstPass($identifier, $identifierValue)
4855
$cacheItemPool->getItem($key)->shouldBeCalled()->willReturn($cacheItem);
4956
$cacheItemPool->save($cacheItem)->shouldBeCalled();
5057

51-
$dummy = new Dummy();
52-
$dummy->setId($identifier);
53-
5458
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
55-
$decoration->getIdentifiersFromItem($dummy)->shouldBeCalled()->willReturn(['id' => $identifierValue]);
59+
$decoration->getIdentifiersFromItem($item)->shouldBeCalled()->willReturn($expected);
5660

57-
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null);
61+
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null, $this->getResourceClassResolver());
5862

59-
$expectedResult = ['id' => $identifierValue];
60-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy));
61-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy), 'Trigger the local cache');
63+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item));
64+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item), 'Trigger the local cache');
6265

6366
$decoration->getIdentifiersFromResourceClass(Dummy::class)->shouldBeCalled()->willReturn(['id']);
6467

6568
$expectedResult = ['id'];
66-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromResourceClass(Dummy::class));
67-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromResourceClass(Dummy::class), 'Trigger the local cache');
69+
$this->assertSame($expectedResult, $identifiersExtractor->getIdentifiersFromResourceClass(Dummy::class));
70+
$this->assertSame($expectedResult, $identifiersExtractor->getIdentifiersFromResourceClass(Dummy::class), 'Trigger the local cache');
6871
}
6972

7073
/**
71-
* @dataProvider identifiersProvider
74+
* @dataProvider itemProvider
7275
*/
73-
public function testSecondPass($identifier, $identifierValue)
76+
public function testSecondPass($item, $expected)
7477
{
7578
$key = 'iri_identifiers'.md5(Dummy::class);
7679

@@ -81,20 +84,49 @@ public function testSecondPass($identifier, $identifierValue)
8184
$cacheItemPool = $this->prophesize(CacheItemPoolInterface::class);
8285
$cacheItemPool->getItem($key)->shouldBeCalled()->willReturn($cacheItem);
8386

87+
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
88+
$decoration->getIdentifiersFromItem($item)->shouldNotBeCalled();
89+
90+
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null, $this->getResourceClassResolver());
91+
92+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item));
93+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item), 'Trigger the local cache');
94+
}
95+
96+
public function identifiersRelatedProvider()
97+
{
98+
$related = new RelatedDummy();
99+
$related->setId($relatedId = 2);
100+
84101
$dummy = new Dummy();
85-
$dummy->setId($identifier);
102+
$dummy->setId($id = 1);
103+
$dummy->setRelatedDummy($related);
86104

87-
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
88-
$decoration->getIdentifiersFromItem($dummy)->shouldNotBeCalled();
105+
yield [$dummy, ['id' => $id, 'relatedDummy' => $relatedId]];
89106

90-
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null);
107+
$related = new RelatedDummy();
108+
$related->setId($relatedId = 1);
91109

92-
$expectedResult = ['id' => $identifierValue];
93-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy));
94-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy), 'Trigger the local cache');
110+
$dummy = new Dummy();
111+
$dummy->setId($id = new Uuid());
112+
$dummy->setRelatedDummy($related);
113+
114+
yield [$dummy, ['id' => $id, 'relatedDummy' => $relatedId]];
115+
116+
$related = new RelatedDummy();
117+
$related->setId($relatedId = new Uuid());
118+
119+
$dummy = new Dummy();
120+
$dummy->setId($id = new Uuid());
121+
$dummy->setRelatedDummy($related);
122+
123+
yield [$dummy, ['id' => $id, 'relatedDummy' => $relatedId]];
95124
}
96125

97-
public function testFirstPassWithRelated()
126+
/**
127+
* @dataProvider identifiersRelatedProvider
128+
*/
129+
public function testFirstPassWithRelated($item, $expected)
98130
{
99131
$key = 'iri_identifiers'.md5(Dummy::class);
100132
$keyRelated = 'iri_identifiers'.md5(RelatedDummy::class);
@@ -110,24 +142,19 @@ public function testFirstPassWithRelated()
110142
$cacheItemPool->getItem($key)->shouldBeCalled()->willReturn($cacheItem);
111143
$cacheItemPool->getItem($keyRelated)->shouldBeCalled()->willReturn($cacheItemRelated);
112144

113-
$related = new RelatedDummy();
114-
$related->setId(1);
115-
116-
$dummy = new Dummy();
117-
$dummy->setId(1);
118-
$dummy->setRelatedDummy($related);
119-
120145
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
121-
$decoration->getIdentifiersFromItem($dummy)->shouldBeCalled()->willReturn(['id' => 1, 'relatedDummy' => 1]);
146+
$decoration->getIdentifiersFromItem($item)->shouldBeCalled()->willReturn($expected);
122147

123-
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null);
148+
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null, $this->getResourceClassResolver());
124149

125-
$expectedResult = ['id' => 1, 'relatedDummy' => 1];
126-
$this->assertEquals(['id' => 1, 'relatedDummy' => 1], $identifiersExtractor->getIdentifiersFromItem($dummy));
127-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy), 'Trigger the local cache');
150+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item));
151+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item), 'Trigger the local cache');
128152
}
129153

130-
public function testSecondPassWithRelated()
154+
/**
155+
* @dataProvider identifiersRelatedProvider
156+
*/
157+
public function testSecondPassWithRelated($item, $expected)
131158
{
132159
$key = 'iri_identifiers'.md5(Dummy::class);
133160
$keyRelated = 'iri_identifiers'.md5(RelatedDummy::class);
@@ -144,20 +171,38 @@ public function testSecondPassWithRelated()
144171
$cacheItemPool->getItem($key)->shouldBeCalled()->willReturn($cacheItem);
145172
$cacheItemPool->getItem($keyRelated)->shouldBeCalled()->willReturn($cacheItemRelated);
146173

147-
$related = new RelatedDummy();
148-
$related->setId(1);
174+
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
175+
$decoration->getIdentifiersFromItem($item)->shouldNotBeCalled();
149176

150-
$dummy = new Dummy();
151-
$dummy->setId(1);
152-
$dummy->setRelatedDummy($related);
177+
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null, $this->getResourceClassResolver());
178+
179+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item));
180+
$this->assertSame($expected, $identifiersExtractor->getIdentifiersFromItem($item), 'Trigger the local cache');
181+
}
153182

183+
/**
184+
* @group legacy
185+
* @expectedDeprecation Not injecting ApiPlatform\Core\Api\ResourceClassResolverInterface in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.
186+
*/
187+
public function testDeprecationResourceClassResolver()
188+
{
189+
$cacheItemPool = $this->prophesize(CacheItemPoolInterface::class);
154190
$decoration = $this->prophesize(IdentifiersExtractorInterface::class);
155-
$decoration->getIdentifiersFromItem($dummy)->shouldNotBeCalled();
156191

157192
$identifiersExtractor = new CachedIdentifiersExtractor($cacheItemPool->reveal(), $decoration->reveal(), null);
193+
}
194+
195+
private function getResourceClassResolver()
196+
{
197+
$resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
198+
$resourceClassResolver->isResourceClass(Argument::type('string'))->will(function ($args) {
199+
if (Uuid::class === $args[0]) {
200+
return false;
201+
}
202+
203+
return true;
204+
});
158205

159-
$expectedResult = ['id' => 1, 'relatedDummy' => 1];
160-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy));
161-
$this->assertEquals($expectedResult, $identifiersExtractor->getIdentifiersFromItem($dummy), 'Trigger the local cache');
206+
return $resourceClassResolver->reveal();
162207
}
163208
}

0 commit comments

Comments
 (0)