Skip to content

Commit 09aff4b

Browse files
author
abluchet
committed
Implement ItemIdentifiersExtractor to cache identifiers properties per resource
1 parent 38d6a6d commit 09aff4b

File tree

6 files changed

+235
-56
lines changed

6 files changed

+235
-56
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@
5050
<argument type="service" id="api_platform.route_name_resolver" />
5151
<argument type="service" id="api_platform.router" />
5252
<argument type="service" id="api_platform.property_accessor" />
53+
<argument type="service" id="api_platform.item_identifiers_extractor.cached" />
54+
</service>
55+
56+
<service id="api_platform.item_identifiers_extractor" class="ApiPlatform\Core\Bridge\Symfony\Routing\ItemIdentifiersExtractor">
57+
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
58+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
59+
<argument type="service" id="api_platform.property_accessor" />
60+
</service>
61+
62+
<service id="api_platform.item_identifiers_extractor.cached" class="ApiPlatform\Core\Bridge\Symfony\Routing\CachedItemIdentifiersExtractor" decorates="api_platform.item_identifiers_extractor">
63+
<argument type="service" id="api_platform.cache.item_identifiers_extractor" />
64+
<argument type="service" id="api_platform.property_accessor" />
65+
<argument type="service" id="api_platform.item_identifiers_extractor.cached.inner" />
5366
</service>
5467

5568
<!-- Serializer -->
@@ -184,6 +197,10 @@
184197
<service id="api_platform.cache.route_name_resolver" parent="cache.system" public="false">
185198
<tag name="cache.pool" />
186199
</service>
200+
201+
<service id="api_platform.cache.item_identifiers_extractor" parent="cache.system" public="false">
202+
<tag name="cache.pool" />
203+
</service>
187204
</services>
188205

189206
</container>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
namespace ApiPlatform\Core\Bridge\Symfony\Routing;
13+
14+
use ApiPlatform\Core\Exception\CacheException;
15+
use ApiPlatform\Core\Util\ClassInfoTrait;
16+
use Psr\Cache\CacheItemPoolInterface;
17+
use Symfony\Component\PropertyAccess\PropertyAccess;
18+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
19+
20+
class CachedItemIdentifiersExtractor implements ItemIdentifiersExtractorInterface
21+
{
22+
use ClassInfoTrait;
23+
24+
const CACHE_KEY_PREFIX = 'iri_converter';
25+
26+
private $cacheItemPool;
27+
private $propertyAccessor;
28+
private $decorated;
29+
30+
public function __construct(CacheItemPoolInterface $cacheItemPool, PropertyAccessorInterface $propertyAccessor = null, ItemIdentifiersExtractorInterface $decorated = null)
31+
{
32+
$this->cacheItemPool = $cacheItemPool;
33+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
34+
$this->decorated = $decorated;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getIdentifiersFromItem($item): array
41+
{
42+
$identifiers = [];
43+
$resourceClass = $this->getObjectClass($item);
44+
45+
$cacheKey = self::CACHE_KEY_PREFIX.md5($resourceClass);
46+
47+
try {
48+
$cacheItem = $this->cacheItemPool->getItem($cacheKey);
49+
50+
if ($cacheItem->isHit()) {
51+
foreach ($cacheItem->get() as $propertyName) {
52+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
53+
54+
if (!is_object($identifiers[$propertyName])) {
55+
continue;
56+
}
57+
58+
$relatedItem = $identifiers[$propertyName];
59+
$relatedCacheKey = self::CACHE_KEY_PREFIX.md5($this->getObjectClass($relatedItem));
60+
61+
$relatedCacheItem = $this->cacheItemPool->getItem($relatedCacheKey);
62+
63+
if (!$relatedCacheItem->isHit()) {
64+
throw new CacheException('No relation cache item founded, we need more cache to continue.');
65+
}
66+
67+
unset($identifiers[$propertyName]);
68+
69+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedCacheItem->get()[0]);
70+
}
71+
72+
return $identifiers;
73+
}
74+
} catch (CacheException $e) {
75+
// do nothing
76+
}
77+
78+
$identifiers = $this->decorated->getIdentifiersFromItem($item);
79+
80+
if (isset($cacheItem)) {
81+
try {
82+
$cacheItem->set(array_keys($identifiers));
83+
$this->cacheItemPool->save($cacheItem);
84+
} catch (CacheException $e) {
85+
// do nothing
86+
}
87+
}
88+
89+
return $identifiers;
90+
}
91+
}

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
1717
use ApiPlatform\Core\Exception\InvalidArgumentException;
1818
use ApiPlatform\Core\Exception\ItemNotFoundException;
19-
use ApiPlatform\Core\Exception\RuntimeException;
2019
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2120
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2221
use ApiPlatform\Core\Util\ClassInfoTrait;
@@ -40,15 +39,23 @@ final class IriConverter implements IriConverterInterface
4039
private $routeNameResolver;
4140
private $router;
4241
private $propertyAccessor;
42+
private $itemIdentifiersExtractor;
4343

44-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null)
44+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null, ItemIdentifiersExtractorInterface $itemIdentifiersExtractor = null)
4545
{
4646
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
4747
$this->propertyMetadataFactory = $propertyMetadataFactory;
4848
$this->itemDataProvider = $itemDataProvider;
4949
$this->routeNameResolver = $routeNameResolver;
5050
$this->router = $router;
5151
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
52+
53+
if (!$itemIdentifiersExtractor) {
54+
@trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3');
55+
$this->itemIdentifiersExtractor = new ItemIdentifiersExtractor($this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $this->propertyAccessor);
56+
} else {
57+
$this->itemIdentifiersExtractor = $itemIdentifiersExtractor;
58+
}
5259
}
5360

5461
/**
@@ -81,7 +88,7 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface
8188
$resourceClass = $this->getObjectClass($item);
8289
$routeName = $this->routeNameResolver->getRouteName($resourceClass, false);
8390

84-
$identifiers = $this->generateIdentifiersUrl($this->getIdentifiersFromItem($item));
91+
$identifiers = $this->generateIdentifiersUrl($this->itemIdentifiersExtractor->getIdentifiersFromItem($item));
8592

8693
return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
8794
}
@@ -98,59 +105,6 @@ public function getIriFromResourceClass(string $resourceClass, int $referenceTyp
98105
}
99106
}
100107

101-
/**
102-
* Find identifiers from an Item (Object).
103-
*
104-
* @param object $item
105-
*
106-
* @throws RuntimeException
107-
*
108-
* @return array
109-
*/
110-
private function getIdentifiersFromItem($item): array
111-
{
112-
$identifiers = [];
113-
$resourceClass = $this->getObjectClass($item);
114-
115-
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
116-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
117-
118-
$identifier = $propertyMetadata->isIdentifier();
119-
if (null === $identifier || false === $identifier) {
120-
continue;
121-
}
122-
123-
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
124-
125-
if (!is_object($identifiers[$propertyName])) {
126-
continue;
127-
}
128-
129-
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
130-
$relatedItem = $identifiers[$propertyName];
131-
132-
unset($identifiers[$propertyName]);
133-
134-
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {
135-
$propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName);
136-
137-
if ($propertyMetadata->isIdentifier()) {
138-
if (isset($identifiers[$propertyName])) {
139-
throw new RuntimeException(sprintf('Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
140-
}
141-
142-
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName);
143-
}
144-
}
145-
146-
if (!isset($identifiers[$propertyName])) {
147-
throw new RuntimeException(sprintf('No identifier found in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
148-
}
149-
}
150-
151-
return $identifiers;
152-
}
153-
154108
/**
155109
* Generate the identifier url.
156110
*
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
namespace ApiPlatform\Core\Bridge\Symfony\Routing;
13+
14+
use ApiPlatform\Core\Exception\RuntimeException;
15+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
16+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
17+
use ApiPlatform\Core\Util\ClassInfoTrait;
18+
use Symfony\Component\PropertyAccess\PropertyAccess;
19+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20+
21+
class ItemIdentifiersExtractor implements ItemIdentifiersExtractorInterface
22+
{
23+
use ClassInfoTrait;
24+
25+
private $propertyNameCollectionFactory;
26+
private $propertyMetadataFactory;
27+
private $propertyAccessor;
28+
29+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null)
30+
{
31+
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
32+
$this->propertyMetadataFactory = $propertyMetadataFactory;
33+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function getIdentifiersFromItem($item): array
40+
{
41+
$identifiers = [];
42+
$resourceClass = $this->getObjectClass($item);
43+
44+
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
45+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
46+
47+
$identifier = $propertyMetadata->isIdentifier();
48+
if (null === $identifier || false === $identifier) {
49+
continue;
50+
}
51+
52+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
53+
54+
if (!is_object($identifiers[$propertyName])) {
55+
continue;
56+
}
57+
58+
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
59+
$relatedItem = $identifiers[$propertyName];
60+
61+
unset($identifiers[$propertyName]);
62+
63+
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {
64+
$propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName);
65+
66+
if ($propertyMetadata->isIdentifier()) {
67+
if (isset($identifiers[$propertyName])) {
68+
throw new RuntimeException(sprintf('Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
69+
}
70+
71+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName);
72+
}
73+
}
74+
75+
if (!isset($identifiers[$propertyName])) {
76+
throw new RuntimeException(sprintf('No identifier found in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
77+
}
78+
}
79+
80+
return $identifiers;
81+
}
82+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
namespace ApiPlatform\Core\Bridge\Symfony\Routing;
13+
14+
interface ItemIdentifiersExtractorInterface
15+
{
16+
/**
17+
* Find identifiers from an Item (Object).
18+
*
19+
* @param object $item
20+
*
21+
* @throws RuntimeException
22+
*
23+
* @return array
24+
*/
25+
public function getIdentifiersFromItem($item): array;
26+
}

src/Exception/CacheException.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Exception;
4+
5+
use Psr\Cache\CacheException as CacheExceptionInterface;
6+
7+
class CacheException extends \Exception implements CacheExceptionInterface
8+
{
9+
}

0 commit comments

Comments
 (0)