Skip to content

Commit 3924aa8

Browse files
committed
Add Read Stage
1 parent 092818a commit 3924aa8

File tree

8 files changed

+199
-61
lines changed

8 files changed

+199
-61
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@
153153
<service id="api_platform.path_segment_name_generator.underscore" class="ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator" public="false" />
154154
<service id="api_platform.path_segment_name_generator.dash" class="ApiPlatform\Core\Operation\DashPathSegmentNameGenerator" public="false" />
155155

156+
<!-- Stages -->
157+
158+
<service id="api_platform.stage.read" class="ApiPlatform\Core\Stage\ReadStage">
159+
<argument type="service" id="api_platform.collection_data_provider" />
160+
<argument type="service" id="api_platform.item_data_provider" />
161+
<argument type="service" id="api_platform.subresource_data_provider" />
162+
<argument type="service" id="api_platform.identifier.converter" />
163+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
164+
</service>
165+
156166
<!-- Event listeners -->
157167

158168
<!-- kernel.request priority must be < 8 to be executed after the Firewall -->
@@ -165,12 +175,8 @@
165175
</service>
166176

167177
<service id="api_platform.listener.request.read" class="ApiPlatform\Core\EventListener\ReadListener">
168-
<argument type="service" id="api_platform.collection_data_provider" />
169-
<argument type="service" id="api_platform.item_data_provider" />
170-
<argument type="service" id="api_platform.subresource_data_provider" />
178+
<argument type="service" id="api_platform.stage.read" />
171179
<argument type="service" id="api_platform.serializer.context_builder" />
172-
<argument type="service" id="api_platform.identifier.converter" />
173-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
174180

175181
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="4" />
176182
</service>

src/EventListener/ReadListener.php

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515

1616
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
1717
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
18-
use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
1918
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
20-
use ApiPlatform\Core\Exception\InvalidIdentifierException;
21-
use ApiPlatform\Core\Exception\RuntimeException;
19+
use ApiPlatform\Core\Exception\NotFoundException;
2220
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
2321
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
24-
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
2522
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
23+
use ApiPlatform\Core\Stage\ReadStage;
24+
use ApiPlatform\Core\Stage\ReadStageInterface;
2625
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2726
use ApiPlatform\Core\Util\RequestParser;
2827
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@@ -35,21 +34,29 @@
3534
*/
3635
final class ReadListener
3736
{
38-
use OperationDataProviderTrait;
39-
use ToggleableOperationAttributeTrait;
40-
4137
public const OPERATION_ATTRIBUTE_KEY = 'read';
4238

39+
private $readStage;
4340
private $serializerContextBuilder;
4441

45-
public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
42+
public function __construct(/*CollectionDataProviderInterface */$readStage, /*ItemDataProviderInterface */$serializerContextBuilder/*, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null*/)
4643
{
47-
$this->collectionDataProvider = $collectionDataProvider;
48-
$this->itemDataProvider = $itemDataProvider;
49-
$this->subresourceDataProvider = $subresourceDataProvider;
44+
$this->readStage = $readStage;
5045
$this->serializerContextBuilder = $serializerContextBuilder;
51-
$this->identifierConverter = $identifierConverter;
52-
$this->resourceMetadataFactory = $resourceMetadataFactory;
46+
47+
if (\func_num_args() > 2) {
48+
@trigger_error(sprintf('Not injecting only "%s" and "%s" is deprecated since API Platform 2.5 and will not be possible anymore in API Platform 3', ReadStageInterface::class, SerializerContextBuilderInterface::class), E_USER_DEPRECATED);
49+
50+
$collectionDataProvider = $readStage;
51+
$itemDataProvider = $serializerContextBuilder;
52+
$subresourceDataProvider = \func_get_arg(2);
53+
$serializerContextBuilder = \func_get_arg(3);
54+
$identifierConverter = \func_get_arg(4);
55+
$resourceMetadataFactory = \func_get_arg(5);
56+
57+
$this->readStage = new ReadStage($collectionDataProvider, $itemDataProvider, $subresourceDataProvider, $identifierConverter, $resourceMetadataFactory);
58+
$this->serializerContextBuilder = $serializerContextBuilder;
59+
}
5360
}
5461

5562
/**
@@ -60,61 +67,33 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
6067
public function onKernelRequest(GetResponseEvent $event): void
6168
{
6269
$request = $event->getRequest();
63-
if (
64-
!($attributes = RequestAttributesExtractor::extractAttributes($request))
65-
|| !$attributes['receive']
66-
|| $request->isMethod('POST') && isset($attributes['collection_operation_name'])
67-
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
68-
) {
69-
return;
70-
}
71-
72-
if (null === $filters = $request->attributes->get('_api_filters')) {
70+
$attributes = RequestAttributesExtractor::extractAttributes($request);
71+
$parameters = $request->attributes->all();
72+
if (null === $filters = ($parameters['_api_filters'] ?? null)) {
7373
$queryString = RequestParser::getQueryString($request);
7474
$filters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
7575
}
76-
77-
$context = null === $filters ? [] : ['filters' => $filters];
76+
$normalizationContext = [];
7877
if ($this->serializerContextBuilder) {
79-
// Builtin data providers are able to use the serialization context to automatically add join clauses
80-
$context += $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
81-
$request->attributes->set('_api_normalization_context', $normalizationContext);
82-
}
83-
84-
if (isset($attributes['collection_operation_name'])) {
85-
$request->attributes->set('data', $this->getCollectionData($attributes, $context));
86-
87-
return;
88-
}
89-
90-
$data = [];
91-
92-
if ($this->identifierConverter) {
93-
$context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true;
78+
$normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
9479
}
9580

9681
try {
97-
$identifiers = $this->extractIdentifiers($request->attributes->all(), $attributes);
98-
99-
if (isset($attributes['item_operation_name'])) {
100-
$data = $this->getItemData($identifiers, $attributes, $context);
101-
} elseif (isset($attributes['subresource_operation_name'])) {
102-
// Legacy
103-
if (null === $this->subresourceDataProvider) {
104-
throw new RuntimeException('No subresource data provider.');
105-
}
106-
107-
$data = $this->getSubresourceData($identifiers, $attributes, $context);
108-
}
109-
} catch (InvalidIdentifierException $e) {
110-
throw new NotFoundHttpException('Not found, because of an invalid identifier configuration', $e);
82+
$data = $this->readStage->apply($attributes, $parameters, $filters, $request->getMethod(), $normalizationContext);
83+
} catch (NotFoundException $e) {
84+
throw new NotFoundHttpException($e->getMessage(), $e->getPrevious());
11185
}
11286

11387
if (null === $data) {
114-
throw new NotFoundHttpException('Not Found');
88+
return;
89+
}
90+
91+
if ($normalizationContext) {
92+
// Builtin data providers are able to use the serialization context to automatically add join clauses
93+
$request->attributes->set('_api_normalization_context', $normalizationContext);
11594
}
11695

11796
$request->attributes->set('data', $data);
118-
$request->attributes->set('previous_data', \is_object($data) ? clone $data : $data);
97+
$request->attributes->set('previous_data', \is_object($data) && (new \ReflectionClass(\get_class($data)))->isCloneable() ? clone $data : $data);
11998
}
12099
}

src/Exception/NotFoundException.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Core\Exception;
15+
16+
/**
17+
* @author Alan Poulain <[email protected]>
18+
*/
19+
final class NotFoundException extends \Exception implements ExceptionInterface
20+
{
21+
}

src/Serializer/SerializerContextBuilder.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
4545
throw new RuntimeException('Request attributes are not valid.');
4646
}
4747

48+
if (!isset($attributes['resource_class'])) {
49+
return [];
50+
}
51+
4852
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
4953
$key = $normalization ? 'normalization_context' : 'denormalization_context';
5054
if (isset($attributes['collection_operation_name'])) {

src/Serializer/SerializerFilterContextBuilder.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
4747
throw new RuntimeException('Request attributes are not valid.');
4848
}
4949

50+
if (!isset($attributes['resource_class'])) {
51+
return [];
52+
}
53+
5054
$context = $this->decorated->createFromRequest($request, $normalization, $attributes);
5155
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
5256

src/Stage/ReadStage.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Core\Stage;
15+
16+
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
17+
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
18+
use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
19+
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
20+
use ApiPlatform\Core\Exception\InvalidIdentifierException;
21+
use ApiPlatform\Core\Exception\NotFoundException;
22+
use ApiPlatform\Core\Exception\RuntimeException;
23+
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
24+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
25+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
26+
27+
/**
28+
* Retrieves data from the applicable data provider.
29+
*
30+
* @author Alan Poulain <[email protected]>
31+
*/
32+
final class ReadStage implements ReadStageInterface
33+
{
34+
use OperationDataProviderTrait;
35+
use ToggleableOperationAttributeTrait;
36+
37+
public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
38+
{
39+
$this->collectionDataProvider = $collectionDataProvider;
40+
$this->itemDataProvider = $itemDataProvider;
41+
$this->subresourceDataProvider = $subresourceDataProvider;
42+
$this->identifierConverter = $identifierConverter;
43+
$this->resourceMetadataFactory = $resourceMetadataFactory;
44+
}
45+
46+
/**
47+
* {@inheritdoc}
48+
*/
49+
public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext)
50+
{
51+
if (!isset($attributes['resource_class'])
52+
|| !$attributes['receive']
53+
|| ('POST' === $method && isset($attributes['collection_operation_name']))
54+
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
55+
) {
56+
return null;
57+
}
58+
59+
$context = null === $filters ? [] : ['filters' => $filters];
60+
$context += $normalizationContext ?? [];
61+
62+
if (isset($attributes['collection_operation_name'])) {
63+
return $this->getCollectionData($attributes, $context);
64+
}
65+
66+
$data = [];
67+
68+
if ($this->identifierConverter) {
69+
$context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true;
70+
}
71+
72+
try {
73+
$identifiers = $this->extractIdentifiers($parameters, $attributes);
74+
75+
if (isset($attributes['item_operation_name'])) {
76+
$data = $this->getItemData($identifiers, $attributes, $context);
77+
} elseif (isset($attributes['subresource_operation_name'])) {
78+
if (null === $this->subresourceDataProvider) {
79+
throw new RuntimeException('No subresource data provider.');
80+
}
81+
82+
$data = $this->getSubresourceData($identifiers, $attributes, $context);
83+
}
84+
} catch (InvalidIdentifierException $e) {
85+
throw new NotFoundException('Not found, because of an invalid identifier configuration', 0, $e);
86+
}
87+
88+
if (null === $data) {
89+
throw new NotFoundException('Not Found');
90+
}
91+
92+
return $data;
93+
}
94+
}

src/Stage/ReadStageInterface.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/*
3+
*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Core\Stage;
15+
16+
/**
17+
* Retrieves data from the applicable data provider.
18+
*
19+
* @author Alan Poulain <[email protected]>
20+
*/
21+
interface ReadStageInterface
22+
{
23+
public const OPERATION_ATTRIBUTE_KEY = 'read';
24+
25+
/**
26+
* @return object|iterable|null
27+
*/
28+
public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext);
29+
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ private function getPartialContainerBuilderProphecy()
861861
'api_platform.identifier.integer',
862862
'api_platform.identifier.uuid_normalizer',
863863
'api_platform.item_data_provider',
864+
'api_platform.stage.read',
864865
'api_platform.listener.exception',
865866
'api_platform.listener.exception.validation',
866867
'api_platform.listener.request.add_format',

0 commit comments

Comments
 (0)