diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.xml b/src/Symfony/Bundle/Resources/config/symfony/events.xml
index 53395b92c9d..4e86b56780e 100644
--- a/src/Symfony/Bundle/Resources/config/symfony/events.xml
+++ b/src/Symfony/Bundle/Resources/config/symfony/events.xml
@@ -42,6 +42,7 @@
+
@@ -68,8 +69,6 @@
- null
- null
@@ -77,7 +76,6 @@
- null
diff --git a/src/Symfony/EventListener/AddFormatListener.php b/src/Symfony/EventListener/AddFormatListener.php
index 569f58bf07f..fe788318360 100644
--- a/src/Symfony/EventListener/AddFormatListener.php
+++ b/src/Symfony/EventListener/AddFormatListener.php
@@ -102,7 +102,8 @@ public function onKernelRequest(RequestEvent $event): void
return;
}
- if (!($request->attributes->has('_api_resource_class')
+ if (!(
+ $request->attributes->has('_api_resource_class')
|| $request->attributes->getBoolean('_api_respond', false)
|| $request->attributes->getBoolean('_graphql', false)
)) {
diff --git a/src/Symfony/EventListener/DeserializeListener.php b/src/Symfony/EventListener/DeserializeListener.php
index 49098cd06d5..bf547d32b44 100644
--- a/src/Symfony/EventListener/DeserializeListener.php
+++ b/src/Symfony/EventListener/DeserializeListener.php
@@ -50,10 +50,7 @@ final class DeserializeListener
private SerializerInterface $serializer;
private ?ProviderInterface $provider = null;
- /**
- * @param ProviderInterface|SerializerInterface $serializer
- */
- public function __construct($serializer, private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null)
+ public function __construct(ProviderInterface|SerializerInterface $serializer, private readonly null|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null)
{
if ($serializer instanceof ProviderInterface) {
$this->provider = $serializer;
@@ -62,6 +59,12 @@ public function __construct($serializer, private readonly ?SerializerContextBuil
$this->serializer = $serializer;
}
+ if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) {
+ $resourceMetadataFactory = $serializerContextBuilder;
+ } else {
+ trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class);
+ }
+
$this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
if (null === $this->translator) {
$this->translator = new class() implements TranslatorInterface, LocaleAwareInterface {
diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php
index f2f88c6caf4..3490e4d6f2b 100644
--- a/src/Symfony/EventListener/ErrorListener.php
+++ b/src/Symfony/EventListener/ErrorListener.php
@@ -102,45 +102,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
}
$dup = parent::duplicateRequest($exception, $request);
- if ($this->resourceMetadataCollectionFactory) {
- if ($this->resourceClassResolver?->isResourceClass($exception::class)) {
- $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);
-
- $operation = null;
- foreach ($resourceCollection as $resource) {
- foreach ($resource->getOperations() as $op) {
- foreach ($op->getOutputFormats() as $key => $value) {
- if ($key === $format) {
- $operation = $op;
- break 3;
- }
- }
- }
- }
-
- // No operation found for the requested format, we take the first available
- if (!$operation) {
- $operation = $resourceCollection->getOperation();
- }
- if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
- $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
- $operation = $operation->withStatus($statusCode);
- }
- } else {
- // Create a generic, rfc7807 compatible error according to the wanted format
- $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
- // status code may be overriden by the exceptionToStatus option
- $statusCode = 500;
- if ($operation instanceof HttpOperation) {
- $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
- $operation = $operation->withStatus($statusCode);
- }
- }
- } else {
- /** @var HttpOperation $operation */
- $operation = new ErrorOperation(name: '_api_errors_problem', class: Error::class, outputFormats: ['jsonld' => ['application/problem+json']], normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]);
- $operation = $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
- }
+ $operation = $this->initializeExceptionOperation($request, $exception, $format, $apiOperation);
if (null === $operation->getProvider()) {
$operation = $operation->withProvider('api_platform.state.error_provider');
@@ -171,6 +133,9 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re
return $dup;
}
+ /**
+ * @return array>
+ */
private function getOperationExceptionToStatus(Request $request): array
{
$attributes = RequestAttributesExtractor::extractAttributes($request);
@@ -244,4 +209,56 @@ private function getFormatOperation(?string $format): string
default => '_api_errors_problem'
};
}
+
+ private function initializeExceptionOperation(?Request $request, \Throwable $exception, string $format, ?HttpOperation $apiOperation): HttpOperation
+ {
+ if (!$this->resourceMetadataCollectionFactory) {
+ /** @var HttpOperation $operation */
+ $operation = new ErrorOperation(
+ name: '_api_errors_problem',
+ class: Error::class,
+ outputFormats: ['jsonld' => ['application/problem+json']],
+ normalizationContext: ['groups' => ['jsonld'], 'skip_null_values' => true]
+ );
+
+ return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
+ }
+
+ if ($this->resourceClassResolver?->isResourceClass($exception::class)) {
+ $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class);
+
+ $operation = null;
+ // TODO: move this to ResourceMetadataCollection?
+ foreach ($resourceCollection as $resource) {
+ foreach ($resource->getOperations() as $op) {
+ foreach ($op->getOutputFormats() as $key => $value) {
+ if ($key === $format) {
+ $operation = $op;
+ break 3;
+ }
+ }
+ }
+ }
+
+ // No operation found for the requested format, we take the first available
+ $operation ??= $resourceCollection->getOperation();
+
+ if ($exception instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) {
+ return $operation->withStatus($this->getStatusCode($apiOperation, $request, $operation, $exception));
+ }
+
+ return $operation;
+ }
+
+ // Create a generic, rfc7807 compatible error according to the wanted format
+ $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format));
+ // status code may be overriden by the exceptionToStatus option
+ $statusCode = 500;
+ if ($operation instanceof HttpOperation) {
+ $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception);
+ $operation = $operation->withStatus($statusCode);
+ }
+
+ return $operation;
+ }
}
diff --git a/src/Symfony/EventListener/SerializeListener.php b/src/Symfony/EventListener/SerializeListener.php
index 6eec5b2fd5d..0a289983ea3 100644
--- a/src/Symfony/EventListener/SerializeListener.php
+++ b/src/Symfony/EventListener/SerializeListener.php
@@ -46,10 +46,11 @@ final class SerializeListener
public const OPERATION_ATTRIBUTE_KEY = 'serialize';
private ?SerializerInterface $serializer = null;
private ?ProcessorInterface $processor = null;
+ private ?SerializerContextBuilderInterface $serializerContextBuilder = null;
public function __construct(
- $serializer,
- private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null,
+ SerializerInterface|ProcessorInterface $serializer,
+ SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface $serializerContextBuilder = null,
ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
private readonly array $errorFormats = [],
// @phpstan-ignore-next-line we don't need this anymore
@@ -62,6 +63,13 @@ public function __construct(
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProcessorInterface::class, self::class, SerializerInterface::class);
}
+ if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) {
+ $resourceMetadataFactory = $serializerContextBuilder;
+ } else {
+ $this->serializerContextBuilder = $serializerContextBuilder;
+ trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class);
+ }
+
$this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
}
diff --git a/src/Symfony/EventListener/ValidateListener.php b/src/Symfony/EventListener/ValidateListener.php
index fb93d048b82..98adb6f6792 100644
--- a/src/Symfony/EventListener/ValidateListener.php
+++ b/src/Symfony/EventListener/ValidateListener.php
@@ -32,13 +32,10 @@ final class ValidateListener
public const OPERATION_ATTRIBUTE_KEY = 'validate';
- private ?ValidatorInterface $validator = null;
- private ?ProviderInterface $provider = null;
+ private ValidatorInterface $validator;
+ private ProviderInterface $provider;
- /**
- * @param ProviderInterface|ValidatorInterface $validator
- */
- public function __construct($validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
+ public function __construct(ProviderInterface|ValidatorInterface $validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
{
if ($validator instanceof ProviderInterface) {
$this->provider = $validator;
diff --git a/src/Symfony/EventListener/WriteListener.php b/src/Symfony/EventListener/WriteListener.php
index 778d90b1303..6c7a42cb497 100644
--- a/src/Symfony/EventListener/WriteListener.php
+++ b/src/Symfony/EventListener/WriteListener.php
@@ -17,6 +17,9 @@
use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface;
use ApiPlatform\Exception\InvalidIdentifierException;
+use ApiPlatform\Metadata\Error;
+use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
+use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
@@ -46,22 +49,32 @@ final class WriteListener
use OperationRequestInitiatorTrait;
use UriVariablesResolverTrait;
+ private LegacyIriConverterInterface|IriConverterInterface|null $iriConverter;
+
/**
* @param ProcessorInterface $processor
*/
public function __construct(
private readonly ProcessorInterface $processor,
- private readonly null|LegacyIriConverterInterface|IriConverterInterface $iriConverter = null,
+ LegacyIriConverterInterface|IriConverterInterface|ResourceMetadataCollectionFactoryInterface $iriConverter = null,
private readonly null|ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver = null,
ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
LegacyUriVariablesConverterInterface|UriVariablesConverterInterface $uriVariablesConverter = null,
) {
- $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->uriVariablesConverter = $uriVariablesConverter;
if ($processor instanceof CallableProcessor) {
trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', WriteProcessor::class, self::class, $processor::class);
}
+
+ if ($iriConverter instanceof ResourceMetadataCollectionFactoryInterface) {
+ $resourceMetadataCollectionFactory = $iriConverter;
+ } else {
+ $this->iriConverter = $iriConverter;
+ trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, IriConverterInterface::class);
+ }
+
+ $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
}
/**
@@ -77,12 +90,20 @@ public function onKernelView(ViewEvent $event): void
return;
}
- if ($operation && $this->processor instanceof WriteProcessor) {
+ if ($operation && !$this->processor instanceof CallableProcessor) {
if (null === $operation->canWrite()) {
$operation = $operation->withWrite(!$request->isMethodSafe());
}
$uriVariables = $request->attributes->get('_api_uri_variables') ?? [];
+ if (!$uriVariables && !$operation instanceof Error && $operation instanceof HttpOperation) {
+ try {
+ $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass());
+ } catch (InvalidIdentifierException|InvalidUriVariableException $e) {
+ throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
+ }
+ }
+
$request->attributes->set('original_data', $this->clone($controllerResult));
$data = $this->processor->process($controllerResult, $operation, $uriVariables, [
'request' => $request,
diff --git a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php
index 83948de314e..11a70b10527 100644
--- a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php
+++ b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php
@@ -40,8 +40,11 @@ public function testFetchOperation(): void
$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
- new RequestEvent($this->createStub(HttpKernelInterface::class),
- new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST)
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']),
+ HttpKernelInterface::MAIN_REQUEST
+ )
);
}
@@ -53,8 +56,11 @@ public function testCallProvider(): void
$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
- new RequestEvent($this->createStub(HttpKernelInterface::class),
- new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']), HttpKernelInterface::MAIN_REQUEST)
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']),
+ HttpKernelInterface::MAIN_REQUEST
+ )
);
}
@@ -66,8 +72,11 @@ public function testNoCallProvider(...$attributes): void
$metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
$listener = new AddFormatListener($provider, $metadata);
$listener->onKernelRequest(
- new RequestEvent($this->createStub(HttpKernelInterface::class),
- new Request([], [], $attributes), HttpKernelInterface::MAIN_REQUEST)
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ new Request([], [], $attributes),
+ HttpKernelInterface::MAIN_REQUEST
+ )
);
}
diff --git a/src/Symfony/Tests/EventListener/DeserializeListenerTest.php b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php
new file mode 100644
index 00000000000..4a2a66ba538
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php
@@ -0,0 +1,129 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\State\ProviderInterface;
+use ApiPlatform\Symfony\EventListener\DeserializeListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class DeserializeListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Post(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $request->setMethod('POST');
+ $listener = new DeserializeListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testCallProvider(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $request->setMethod('POST');
+ $listener = new DeserializeListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallProvider(...$attributes): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->never())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new DeserializeListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ new Request([], [], $attributes),
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testDeserializeFalse(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->never())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Post(deserialize: false), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $request->setMethod('POST');
+ $listener = new DeserializeListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testDeserializeNullWithGetMethod(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->never())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new DeserializeListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_receive' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+}
diff --git a/tests/Symfony/EventListener/ErrorListenerTest.php b/src/Symfony/Tests/EventListener/ErrorListenerTest.php
similarity index 52%
rename from tests/Symfony/EventListener/ErrorListenerTest.php
rename to src/Symfony/Tests/EventListener/ErrorListenerTest.php
index 58d806af2bd..3bc622c1bdf 100644
--- a/tests/Symfony/EventListener/ErrorListenerTest.php
+++ b/src/Symfony/Tests/EventListener/ErrorListenerTest.php
@@ -11,52 +11,53 @@
declare(strict_types=1);
-namespace ApiPlatform\Tests\Symfony\EventListener;
+namespace ApiPlatform\Symfony\Tests\EventListener;
-use ApiPlatform\Api\IdentifiersExtractorInterface;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\IdentifiersExtractorInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ApiResource\Error;
use ApiPlatform\Symfony\EventListener\ErrorListener;
use PHPUnit\Framework\TestCase;
-use Prophecy\Argument;
-use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelInterface;
-final class ErrorListenerTest extends TestCase
+class ErrorListenerTest extends TestCase
{
- use ProphecyTrait;
-
public function testDuplicateException(): void
{
$exception = new \Exception();
$operation = new Get(name: '_api_errors_problem', priority: 0, status: 400);
- $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
- $resourceMetadataCollectionFactory->create(Error::class)
- ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]));
- $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
- $resourceClassResolver->isResourceClass($exception::class)->willReturn(false);
- $kernel = $this->prophesize(KernelInterface::class);
- $kernel->handle(Argument::that(function ($request) {
+ $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $resourceMetadataCollectionFactory->expects($this->once())->method('create')
+ ->with(Error::class)
+ ->willReturn(
+ new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])
+ );
+
+ $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
+ $resourceClassResolver->expects($this->once())->method('isResourceClass')->with($exception::class)->willReturn(false);
+ $kernel = $this->createStub(KernelInterface::class);
+ $kernel->method('handle')->willReturnCallback(function ($request) {
$this->assertTrue($request->attributes->has('_api_original_route'));
$this->assertTrue($request->attributes->has('_api_original_route_params'));
$this->assertTrue($request->attributes->has('_api_requested_operation'));
$this->assertTrue($request->attributes->has('_api_previous_operation'));
$this->assertEquals('_api_errors_problem', $request->attributes->get('_api_operation_name'));
- return true;
- }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response());
+ return new Response();
+ });
+
$request = Request::create('/');
$request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true]));
- $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception);
- $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver->reveal(), problemCompliantErrors: true);
+ $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);
+ $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver, problemCompliantErrors: true);
$errorListener->onKernelException($exceptionEvent);
}
@@ -64,25 +65,29 @@ public function testDuplicateExceptionWithHydra(): void
{
$exception = new \Exception();
$operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400);
- $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
- $resourceMetadataCollectionFactory->create(Error::class)
- ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]));
- $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
- $resourceClassResolver->isResourceClass($exception::class)->willReturn(false);
- $kernel = $this->prophesize(KernelInterface::class);
- $kernel->handle(Argument::that(function ($request) {
+ $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $resourceMetadataCollectionFactory->expects($this->once())->method('create')
+ ->with(Error::class)
+ ->willReturn(
+ new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])
+ );
+ $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
+ $resourceClassResolver->expects($this->once())->method('isResourceClass')->with($exception::class)->willReturn(false);
+
+ $kernel = $this->createStub(KernelInterface::class);
+ $kernel->method('handle')->willReturnCallback(function ($request) {
$this->assertTrue($request->attributes->has('_api_original_route'));
$this->assertTrue($request->attributes->has('_api_original_route_params'));
$this->assertTrue($request->attributes->has('_api_requested_operation'));
$this->assertTrue($request->attributes->has('_api_previous_operation'));
$this->assertEquals('_api_errors_hydra', $request->attributes->get('_api_operation_name'));
- return true;
- }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response());
+ return new Response();
+ });
$request = Request::create('/');
$request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true]));
- $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception);
- $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], null, $resourceClassResolver->reveal());
+ $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);
+ $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], null, $resourceClassResolver);
$errorListener->onKernelException($exceptionEvent);
}
@@ -90,13 +95,18 @@ public function testDuplicateExceptionWithErrorResource(): void
{
$exception = Error::createFromException(new \Exception(), 400);
$operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400, outputFormats: ['jsonld' => ['application/ld+json']]);
- $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
- $resourceMetadataCollectionFactory->create(Error::class)
- ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])]));
- $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
- $resourceClassResolver->isResourceClass(Error::class)->willReturn(true);
- $kernel = $this->prophesize(KernelInterface::class);
- $kernel->handle(Argument::that(function ($request) {
+ $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $resourceMetadataCollectionFactory->expects($this->once())->method('create')
+ ->with(Error::class)
+ ->willReturn(
+ new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])
+ );
+
+ $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
+ $resourceClassResolver->expects($this->once())->method('isResourceClass')->with(Error::class)->willReturn(true);
+
+ $kernel = $this->createStub(KernelInterface::class);
+ $kernel->method('handle')->willReturnCallback(function ($request) {
$this->assertTrue($request->attributes->has('_api_original_route'));
$this->assertTrue($request->attributes->has('_api_original_route_params'));
$this->assertTrue($request->attributes->has('_api_requested_operation'));
@@ -117,33 +127,34 @@ public function testDuplicateExceptionWithErrorResource(): void
],
]);
- return true;
- }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response());
+ return new Response();
+ });
$request = Request::create('/');
$request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true]));
- $exceptionEvent = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $exception);
- $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class);
- $identifiersExtractor->getIdentifiersFromItem($exception, Argument::any())->willReturn(['id' => 1]);
- $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal());
+ $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);
+ $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class);
+ $identifiersExtractor->method('getIdentifiersFromItem')->willReturn(['id' => 1]);
+ $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver);
$errorListener->onKernelException($exceptionEvent);
}
public function testDisableErrorResourceHandling(): void
{
$exception = Error::createFromException(new \Exception(), 400);
- $operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400, outputFormats: ['jsonld' => ['application/ld+json']]);
- $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
- $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
- $kernel = $this->prophesize(KernelInterface::class);
- $kernel->handle(Argument::that(function ($request) {
+ $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $resourceMetadataCollectionFactory->expects($this->never())->method('create');
+ $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
+ $resourceClassResolver->expects($this->never())->method('isResourceClass');
+ $kernel = $this->createStub(KernelInterface::class);
+ $kernel->method('handle')->willReturnCallback(function ($request) {
$this->assertEquals($request->attributes->get('_api_operation'), null);
- return true;
- }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response());
- $exceptionEvent = new ExceptionEvent($kernel->reveal(), Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception);
- $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class);
- $identifiersExtractor->getIdentifiersFromItem($exception, Argument::any())->willReturn(['id' => 1]);
- $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal(), null, false);
+ return new Response();
+ });
+
+ $exceptionEvent = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception);
+ $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class);
+ $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], $identifiersExtractor, $resourceClassResolver, null, false);
$errorListener->onKernelException($exceptionEvent);
}
}
diff --git a/src/Symfony/Tests/EventListener/ReadListenerTest.php b/src/Symfony/Tests/EventListener/ReadListenerTest.php
new file mode 100644
index 00000000000..26c05ae1fbd
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/ReadListenerTest.php
@@ -0,0 +1,150 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\Metadata\UriVariablesConverterInterface;
+use ApiPlatform\State\ProviderInterface;
+use ApiPlatform\Symfony\EventListener\ReadListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class ReadListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Get(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new ReadListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testCallProvider(): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new ReadListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallProvider(...$attributes): void
+ {
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->never())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new ReadListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ new Request([], [], $attributes),
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testReadFalse(): void
+ {
+ $operation = new Get(read: false);
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')->with($operation);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new ReadListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testReadWithUriVariables(): void
+ {
+ $operation = new Get(uriVariables: ['id' => new Link(identifiers: ['id'])], class: 'class');
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')->with($operation->withRead(true), ['id' => 3]);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class);
+ $uriVariablesConverter->expects($this->once())->method('convert')->with(['id' => '3'], 'class')->willReturn(['id' => 3]);
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', 'id' => '3']);
+ $listener = new ReadListener($provider, $metadata, null, $uriVariablesConverter);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public function testReadNullWithPostMethod(): void
+ {
+ $operation = new Post(read: false);
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')->with($operation);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $request->setMethod('POST');
+ $listener = new ReadListener($provider, $metadata);
+ $listener->onKernelRequest(
+ new RequestEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_receive' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+}
diff --git a/src/Symfony/Tests/EventListener/RespondListenerTest.php b/src/Symfony/Tests/EventListener/RespondListenerTest.php
new file mode 100644
index 00000000000..d897198dae3
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/RespondListenerTest.php
@@ -0,0 +1,123 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\State\ProcessorInterface;
+use ApiPlatform\Symfony\EventListener\RespondListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\ViewEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class RespondListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Get(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new RespondListener($metadata, $processor);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessor(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new RespondListener($metadata, $processor);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessorContext(): void
+ {
+ $operation = new Get(class: 'class');
+ $controllerResult = new \stdClass();
+ $originalData = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables, 'original_data' => $originalData]);
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')
+ ->with($controllerResult, $operation, $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class', 'original_data' => $originalData])->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new RespondListener($metadata, $processor);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallProcessor(...$attributes): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->never())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], $attributes);
+ $listener = new RespondListener($metadata, $processor);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_respond' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+}
diff --git a/src/Symfony/Tests/EventListener/SerializeListenerTest.php b/src/Symfony/Tests/EventListener/SerializeListenerTest.php
new file mode 100644
index 00000000000..2ffdae3ebda
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/SerializeListenerTest.php
@@ -0,0 +1,122 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\State\ProcessorInterface;
+use ApiPlatform\Symfony\EventListener\SerializeListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\ViewEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class SerializeListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Get(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new SerializeListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessor(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new SerializeListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessorContext(): void
+ {
+ $operation = new Get(class: 'class');
+ $controllerResult = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]);
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')
+ ->with($controllerResult, $operation->withSerialize(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class'])->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new SerializeListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallProcessor(...$attributes): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->never())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], $attributes);
+ $listener = new SerializeListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_respond' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+}
diff --git a/src/Symfony/Tests/EventListener/ValidateListenerTest.php b/src/Symfony/Tests/EventListener/ValidateListenerTest.php
new file mode 100644
index 00000000000..f686b5e40cf
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/ValidateListenerTest.php
@@ -0,0 +1,167 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\State\ProviderInterface;
+use ApiPlatform\Symfony\EventListener\ValidateListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\ViewEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class ValidateListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $controllerResult = new \stdClass();
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Post(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallprovider(): void
+ {
+ $controllerResult = new \stdClass();
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Post(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallproviderContext(): void
+ {
+ $operation = new Post(class: 'class');
+ $controllerResult = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]);
+ $request->setMethod($operation->getMethod());
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')
+ ->with($operation->withValidate(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testDeleteNoValidate(): void
+ {
+ $operation = new Delete(class: 'class');
+ $controllerResult = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]);
+ $request->setMethod($operation->getMethod());
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')
+ ->with($operation->withValidate(false), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testDeleteForceValidate(): void
+ {
+ $operation = new Delete(class: 'class', validate: true);
+ $controllerResult = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables]);
+ $request->setMethod($operation->getMethod());
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->once())->method('provide')
+ ->with($operation->withValidate(true), $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class']);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallprovider(...$attributes): void
+ {
+ $controllerResult = new \stdClass();
+ $provider = $this->createMock(ProviderInterface::class);
+ $provider->expects($this->never())->method('provide');
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], $attributes);
+ $listener = new ValidateListener($provider, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_respond' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+}
diff --git a/src/Symfony/Tests/EventListener/WriteListenerTest.php b/src/Symfony/Tests/EventListener/WriteListenerTest.php
new file mode 100644
index 00000000000..98280b2f3d8
--- /dev/null
+++ b/src/Symfony/Tests/EventListener/WriteListenerTest.php
@@ -0,0 +1,150 @@
+
+ *
+ * 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\Symfony\Tests\EventListener;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
+use ApiPlatform\Metadata\UriVariablesConverterInterface;
+use ApiPlatform\State\ProcessorInterface;
+use ApiPlatform\Symfony\EventListener\WriteListener;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\ViewEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class WriteListenerTest extends TestCase
+{
+ public function testFetchOperation(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
+ $metadata->expects($this->once())->method('create')->with('class')->willReturn(new ResourceMetadataCollection('class', [
+ new ApiResource(operations: [
+ 'operation' => new Get(),
+ ]),
+ ]));
+
+ $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new WriteListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessor(): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']);
+ $listener = new WriteListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public function testCallProcessorContext(): void
+ {
+ $operation = new Get(class: 'class');
+ $controllerResult = new \stdClass();
+ $originalData = new \stdClass();
+ $uriVariables = ['id' => 3];
+ $returnValue = new \stdClass();
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', '_api_uri_variables' => $uriVariables, 'previous_data' => $originalData]);
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->once())->method('process')
+ ->with($controllerResult, $operation, $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class', 'previous_data' => $originalData])->willReturn($returnValue);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $listener = new WriteListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ $this->assertEquals($returnValue, $request->attributes->get('original_data'));
+ }
+
+ #[DataProvider('provideNonApiAttributes')]
+ public function testNoCallProcessor(...$attributes): void
+ {
+ $controllerResult = new \stdClass();
+ $processor = $this->createMock(ProcessorInterface::class);
+ $processor->expects($this->never())->method('process')->willReturn(new Response());
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $request = new Request([], [], $attributes);
+ $listener = new WriteListener($processor, $metadata);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+
+ public static function provideNonApiAttributes(): array
+ {
+ return [
+ ['_api_resource_class' => 'dummy'],
+ ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'],
+ ['_api_persist' => false, '_api_operation_name' => 'dummy'],
+ [],
+ ];
+ }
+
+ public function testWriteWithUriVariables(): void
+ {
+ $controllerResult = new \stdClass();
+ $operation = new Post(uriVariables: ['id' => new Link(identifiers: ['id'])], class: 'class');
+ $provider = $this->createMock(ProcessorInterface::class);
+ $provider->expects($this->once())->method('process')->with($controllerResult, $operation->withWrite(true), ['id' => 3]);
+ $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class);
+ $uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class);
+ $uriVariablesConverter->expects($this->once())->method('convert')->with(['id' => '3'], 'class')->willReturn(['id' => 3]);
+ $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', 'id' => '3']);
+ $request->setMethod($operation->getMethod());
+ $listener = new WriteListener($provider, $metadata, uriVariablesConverter: $uriVariablesConverter);
+ $listener->onKernelView(
+ new ViewEvent(
+ $this->createStub(HttpKernelInterface::class),
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $controllerResult
+ )
+ );
+ }
+}
diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php
index 839319dc928..cf3fdc8902c 100644
--- a/src/Symfony/Validator/Exception/ValidationException.php
+++ b/src/Symfony/Validator/Exception/ValidationException.php
@@ -44,7 +44,8 @@
normalizationContext: ['groups' => ['json'],
'skip_null_values' => true,
'rfc_7807_compliant_errors' => true,
- ]),
+ ]
+ ),
new ErrorOperation(
name: '_api_validation_errors_hydra',
outputFormats: ['jsonld' => ['application/problem+json']],