diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index c951bfb6176..3cc72bf614c 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -153,6 +153,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -165,12 +175,8 @@
-
-
-
+
-
-
diff --git a/src/EventListener/ReadListener.php b/src/EventListener/ReadListener.php
index 6dfd586211c..fb4daf6a37b 100644
--- a/src/EventListener/ReadListener.php
+++ b/src/EventListener/ReadListener.php
@@ -15,14 +15,13 @@
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
-use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
-use ApiPlatform\Core\Exception\InvalidIdentifierException;
-use ApiPlatform\Core\Exception\RuntimeException;
+use ApiPlatform\Core\Exception\NotFoundException;
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
-use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
+use ApiPlatform\Core\Stage\ReadStage;
+use ApiPlatform\Core\Stage\ReadStageInterface;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use ApiPlatform\Core\Util\RequestParser;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@@ -35,21 +34,29 @@
*/
final class ReadListener
{
- use OperationDataProviderTrait;
- use ToggleableOperationAttributeTrait;
-
public const OPERATION_ATTRIBUTE_KEY = 'read';
+ private $readStage;
private $serializerContextBuilder;
- public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
+ public function __construct(/*CollectionDataProviderInterface */$readStage, /*ItemDataProviderInterface */$serializerContextBuilder/*, SubresourceDataProviderInterface $subresourceDataProvider = null, SerializerContextBuilderInterface $serializerContextBuilder = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null*/)
{
- $this->collectionDataProvider = $collectionDataProvider;
- $this->itemDataProvider = $itemDataProvider;
- $this->subresourceDataProvider = $subresourceDataProvider;
+ $this->readStage = $readStage;
$this->serializerContextBuilder = $serializerContextBuilder;
- $this->identifierConverter = $identifierConverter;
- $this->resourceMetadataFactory = $resourceMetadataFactory;
+
+ if (\func_num_args() > 2) {
+ @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);
+
+ $collectionDataProvider = $readStage;
+ $itemDataProvider = $serializerContextBuilder;
+ $subresourceDataProvider = func_get_arg(2);
+ $serializerContextBuilder = func_get_arg(3);
+ $identifierConverter = func_get_arg(4);
+ $resourceMetadataFactory = func_get_arg(5);
+
+ $this->readStage = new ReadStage($collectionDataProvider, $itemDataProvider, $subresourceDataProvider, $identifierConverter, $resourceMetadataFactory);
+ $this->serializerContextBuilder = $serializerContextBuilder;
+ }
}
/**
@@ -60,61 +67,33 @@ public function __construct(CollectionDataProviderInterface $collectionDataProvi
public function onKernelRequest(GetResponseEvent $event): void
{
$request = $event->getRequest();
- if (
- !($attributes = RequestAttributesExtractor::extractAttributes($request))
- || !$attributes['receive']
- || $request->isMethod('POST') && isset($attributes['collection_operation_name'])
- || $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
- ) {
- return;
- }
-
- if (null === $filters = $request->attributes->get('_api_filters')) {
+ $attributes = RequestAttributesExtractor::extractAttributes($request);
+ $parameters = $request->attributes->all();
+ if (null === $filters = ($parameters['_api_filters'] ?? null)) {
$queryString = RequestParser::getQueryString($request);
$filters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
}
-
- $context = null === $filters ? [] : ['filters' => $filters];
+ $normalizationContext = [];
if ($this->serializerContextBuilder) {
- // Builtin data providers are able to use the serialization context to automatically add join clauses
- $context += $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
- $request->attributes->set('_api_normalization_context', $normalizationContext);
- }
-
- if (isset($attributes['collection_operation_name'])) {
- $request->attributes->set('data', $this->getCollectionData($attributes, $context));
-
- return;
- }
-
- $data = [];
-
- if ($this->identifierConverter) {
- $context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true;
+ $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
}
try {
- $identifiers = $this->extractIdentifiers($request->attributes->all(), $attributes);
-
- if (isset($attributes['item_operation_name'])) {
- $data = $this->getItemData($identifiers, $attributes, $context);
- } elseif (isset($attributes['subresource_operation_name'])) {
- // Legacy
- if (null === $this->subresourceDataProvider) {
- throw new RuntimeException('No subresource data provider.');
- }
-
- $data = $this->getSubresourceData($identifiers, $attributes, $context);
- }
- } catch (InvalidIdentifierException $e) {
- throw new NotFoundHttpException('Not found, because of an invalid identifier configuration', $e);
+ $data = $this->readStage->apply($attributes, $parameters, $filters, $request->getMethod(), $normalizationContext);
+ } catch (NotFoundException $e) {
+ throw new NotFoundHttpException($e->getMessage(), $e->getPrevious());
}
if (null === $data) {
- throw new NotFoundHttpException('Not Found');
+ return;
+ }
+
+ if ($normalizationContext) {
+ // Builtin data providers are able to use the serialization context to automatically add join clauses
+ $request->attributes->set('_api_normalization_context', $normalizationContext);
}
$request->attributes->set('data', $data);
- $request->attributes->set('previous_data', \is_object($data) ? clone $data : $data);
+ $request->attributes->set('previous_data', \is_object($data) && (new \ReflectionClass(\get_class($data)))->isCloneable() ? clone $data : $data);
}
}
diff --git a/src/Exception/NotFoundException.php b/src/Exception/NotFoundException.php
new file mode 100644
index 00000000000..b3889a985f2
--- /dev/null
+++ b/src/Exception/NotFoundException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Exception;
+
+/**
+ * @author Alan Poulain
+ */
+final class NotFoundException extends \Exception implements ExceptionInterface
+{
+}
diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php
index bd14625394d..168c02eb1aa 100644
--- a/src/Serializer/SerializerContextBuilder.php
+++ b/src/Serializer/SerializerContextBuilder.php
@@ -45,6 +45,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
throw new RuntimeException('Request attributes are not valid.');
}
+ if (!isset($attributes['resource_class'])) {
+ return [];
+ }
+
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
$key = $normalization ? 'normalization_context' : 'denormalization_context';
if (isset($attributes['collection_operation_name'])) {
diff --git a/src/Serializer/SerializerFilterContextBuilder.php b/src/Serializer/SerializerFilterContextBuilder.php
index bd40de7dd0e..f2e90aba1bc 100644
--- a/src/Serializer/SerializerFilterContextBuilder.php
+++ b/src/Serializer/SerializerFilterContextBuilder.php
@@ -47,6 +47,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
throw new RuntimeException('Request attributes are not valid.');
}
+ if (!isset($attributes['resource_class'])) {
+ return [];
+ }
+
$context = $this->decorated->createFromRequest($request, $normalization, $attributes);
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
diff --git a/src/Stage/ReadStage.php b/src/Stage/ReadStage.php
new file mode 100644
index 00000000000..b0149314a14
--- /dev/null
+++ b/src/Stage/ReadStage.php
@@ -0,0 +1,94 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Stage;
+
+use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
+use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\DataProvider\OperationDataProviderTrait;
+use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
+use ApiPlatform\Core\Exception\InvalidIdentifierException;
+use ApiPlatform\Core\Exception\NotFoundException;
+use ApiPlatform\Core\Exception\RuntimeException;
+use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
+
+/**
+ * Retrieves data from the applicable data provider.
+ *
+ * @author Alan Poulain
+ */
+final class ReadStage implements ReadStageInterface
+{
+ use OperationDataProviderTrait;
+ use ToggleableOperationAttributeTrait;
+
+ public function __construct(CollectionDataProviderInterface $collectionDataProvider, ItemDataProviderInterface $itemDataProvider, SubresourceDataProviderInterface $subresourceDataProvider = null, IdentifierConverterInterface $identifierConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
+ {
+ $this->collectionDataProvider = $collectionDataProvider;
+ $this->itemDataProvider = $itemDataProvider;
+ $this->subresourceDataProvider = $subresourceDataProvider;
+ $this->identifierConverter = $identifierConverter;
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext)
+ {
+ if (!isset($attributes['resource_class'])
+ || !$attributes['receive']
+ || ('POST' === $method && isset($attributes['collection_operation_name']))
+ || $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
+ ) {
+ return null;
+ }
+
+ $context = null === $filters ? [] : ['filters' => $filters];
+ $context += $normalizationContext ?? [];
+
+ if (isset($attributes['collection_operation_name'])) {
+ return $this->getCollectionData($attributes, $context);
+ }
+
+ $data = [];
+
+ if ($this->identifierConverter) {
+ $context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true;
+ }
+
+ try {
+ $identifiers = $this->extractIdentifiers($parameters, $attributes);
+
+ if (isset($attributes['item_operation_name'])) {
+ $data = $this->getItemData($identifiers, $attributes, $context);
+ } elseif (isset($attributes['subresource_operation_name'])) {
+ if (null === $this->subresourceDataProvider) {
+ throw new RuntimeException('No subresource data provider.');
+ }
+
+ $data = $this->getSubresourceData($identifiers, $attributes, $context);
+ }
+ } catch (InvalidIdentifierException $e) {
+ throw new NotFoundException('Not found, because of an invalid identifier configuration', 0, $e);
+ }
+
+ if (null === $data) {
+ throw new NotFoundException('Not Found');
+ }
+
+ return $data;
+ }
+}
diff --git a/src/Stage/ReadStageInterface.php b/src/Stage/ReadStageInterface.php
new file mode 100644
index 00000000000..9a9ea6cdcac
--- /dev/null
+++ b/src/Stage/ReadStageInterface.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Stage;
+
+/**
+ * Retrieves data from the applicable data provider.
+ *
+ * @author Alan Poulain
+ */
+interface ReadStageInterface
+{
+ public const OPERATION_ATTRIBUTE_KEY = 'read';
+
+ /**
+ * @return object|iterable|null
+ */
+ public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext);
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 4296c5ebaa8..0774d3933a0 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -861,6 +861,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.identifier.integer',
'api_platform.identifier.uuid_normalizer',
'api_platform.item_data_provider',
+ 'api_platform.stage.read',
'api_platform.listener.exception',
'api_platform.listener.exception.validation',
'api_platform.listener.request.add_format',