Skip to content

Commit d89d62d

Browse files
committed
Add Read Stage
1 parent 092818a commit d89d62d

File tree

8 files changed

+190
-56
lines changed

8 files changed

+190
-56
lines changed

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

Lines changed: 11 additions & 0 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 -->
@@ -171,6 +181,7 @@
171181
<argument type="service" id="api_platform.serializer.context_builder" />
172182
<argument type="service" id="api_platform.identifier.converter" />
173183
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
184+
<argument type="service" id="api_platform.stage.read" />
174185

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

src/EventListener/ReadListener.php

Lines changed: 25 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,19 @@
3534
*/
3635
final class ReadListener
3736
{
38-
use OperationDataProviderTrait;
39-
use ToggleableOperationAttributeTrait;
40-
4137
public const OPERATION_ATTRIBUTE_KEY = 'read';
4238

4339
private $serializerContextBuilder;
40+
private $readStage;
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 $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ReadStageInterface $readStage = null)
4643
{
47-
$this->collectionDataProvider = $collectionDataProvider;
48-
$this->itemDataProvider = $itemDataProvider;
49-
$this->subresourceDataProvider = $subresourceDataProvider;
5044
$this->serializerContextBuilder = $serializerContextBuilder;
51-
$this->identifierConverter = $identifierConverter;
52-
$this->resourceMetadataFactory = $resourceMetadataFactory;
45+
$this->readStage = $readStage;
46+
if (null === $readStage) {
47+
@trigger_error(sprintf('Not injecting "%s" is deprecated since API Platform 2.5 and will not be possible anymore in API Platform 3', ReadStageInterface::class), E_USER_DEPRECATED);
48+
$this->readStage = new ReadStage($collectionDataProvider, $itemDataProvider, $subresourceDataProvider, $identifierConverter, $resourceMetadataFactory);
49+
}
5350
}
5451

5552
/**
@@ -60,61 +57,33 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
6057
public function onKernelRequest(GetResponseEvent $event): void
6158
{
6259
$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')) {
60+
$attributes = RequestAttributesExtractor::extractAttributes($request);
61+
$parameters = $request->attributes->all();
62+
if (null === $filters = ($parameters['_api_filters'] ?? null)) {
7363
$queryString = RequestParser::getQueryString($request);
7464
$filters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
7565
}
76-
77-
$context = null === $filters ? [] : ['filters' => $filters];
66+
$normalizationContext = [];
7867
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;
68+
$normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
9469
}
9570

9671
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);
72+
$data = $this->readStage->apply($attributes, $parameters, $filters, $request->getMethod(), $normalizationContext);
73+
} catch (NotFoundException $e) {
74+
throw new NotFoundHttpException($e->getMessage());
11175
}
11276

11377
if (null === $data) {
114-
throw new NotFoundHttpException('Not Found');
78+
return;
79+
}
80+
81+
if ($normalizationContext) {
82+
// Builtin data providers are able to use the serialization context to automatically add join clauses
83+
$request->attributes->set('_api_normalization_context', $normalizationContext);
11584
}
11685

11786
$request->attributes->set('data', $data);
118-
$request->attributes->set('previous_data', \is_object($data) ? clone $data : $data);
87+
$request->attributes->set('previous_data', \is_object($data) && (new \ReflectionClass(\get_class($data)))->isCloneable() ? clone $data : $data);
11988
}
12089
}

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+
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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 and sets it as a request parameter called data.
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+
// Legacy
79+
if (null === $this->subresourceDataProvider) {
80+
throw new RuntimeException('No subresource data provider.');
81+
}
82+
83+
$data = $this->getSubresourceData($identifiers, $attributes, $context);
84+
}
85+
} catch (InvalidIdentifierException $e) {
86+
throw new NotFoundException('Not found, because of an invalid identifier configuration', $e);
87+
}
88+
89+
if (null === $data) {
90+
throw new NotFoundException('Not Found');
91+
}
92+
93+
return $data;
94+
}
95+
}

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 and sets it as a request parameter called data.
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)