diff --git a/features/main/input_output.feature b/features/main/input_output.feature
index 1ff80045a71..46ae576d900 100644
--- a/features/main/input_output.feature
+++ b/features/main/input_output.feature
@@ -245,3 +245,45 @@ Feature: DTO input and output
"""
Then the response status code should be 201
And the response should be empty
+
+ @!mongodb
+ Scenario: Use messenger with an input where the handler gives a synchronous result
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/messenger_with_inputs" with body:
+ """
+ {
+ "var": "test"
+ }
+ """
+ Then the response status code should be 201
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
+ And the JSON should be equal to:
+ """
+ {
+ "@context": "/contexts/MessengerWithInput",
+ "@id": "/messenger_with_inputs/1",
+ "@type": "MessengerWithInput",
+ "id": 1,
+ "name": "test"
+ }
+ """
+
+ @!mongodb
+ Scenario: Use messenger with an input where the handler gives a synchronous Response result
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/messenger_with_responses" with body:
+ """
+ {
+ "var": "test"
+ }
+ """
+ Then the response status code should be 200
+ And the response should be in JSON
+ And the header "Content-Type" should be equal to "application/json"
+ And the JSON should be equal to:
+ """
+ {
+ "data": 123
+ }
+ """
diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php
index d01da67b506..b3dbadb8a21 100644
--- a/src/Annotation/ApiResource.php
+++ b/src/Annotation/ApiResource.php
@@ -43,7 +43,7 @@
* @Attribute("itemOperations", type="array"),
* @Attribute("maximumItemsPerPage", type="int"),
* @Attribute("mercure", type="mixed"),
- * @Attribute("messenger", type="bool"),
+ * @Attribute("messenger", type="mixed"),
* @Attribute("normalizationContext", type="array"),
* @Attribute("openapiContext", type="array"),
* @Attribute("order", type="array"),
@@ -196,7 +196,7 @@ final class ApiResource
/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
- * @var bool
+ * @var bool|string
*/
private $messenger;
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/messenger.xml b/src/Bridge/Symfony/Bundle/Resources/config/messenger.xml
index 3fb063b428f..5117f5e9d41 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/messenger.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/messenger.xml
@@ -13,6 +13,12 @@
+
+
+
+
+
+
diff --git a/src/Bridge/Symfony/Messenger/DataPersister.php b/src/Bridge/Symfony/Messenger/DataPersister.php
index c85d5c9385b..537bc72ae51 100644
--- a/src/Bridge/Symfony/Messenger/DataPersister.php
+++ b/src/Bridge/Symfony/Messenger/DataPersister.php
@@ -54,7 +54,7 @@ public function supports($data, array $context = []): bool
}
if (null !== $operationName = $context['collection_operation_name'] ?? $context['item_operation_name'] ?? null) {
- return true === $resourceMetadata->getTypedOperationAttribute(
+ return false !== $resourceMetadata->getTypedOperationAttribute(
$context['collection_operation_name'] ?? false ? OperationType::COLLECTION : OperationType::ITEM,
$operationName,
'messenger',
@@ -63,7 +63,7 @@ public function supports($data, array $context = []): bool
);
}
- return true === $resourceMetadata->getAttribute('messenger');
+ return false !== $resourceMetadata->getAttribute('messenger', false);
}
/**
diff --git a/src/Bridge/Symfony/Messenger/DataTransformer.php b/src/Bridge/Symfony/Messenger/DataTransformer.php
new file mode 100644
index 00000000000..48cd6fdd4fb
--- /dev/null
+++ b/src/Bridge/Symfony/Messenger/DataTransformer.php
@@ -0,0 +1,70 @@
+
+ *
+ * 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\Bridge\Symfony\Messenger;
+
+use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+
+/**
+ * Transforms an input that implements the InputMessage interface
+ * to itself. This gives the ability to send the Input to a
+ * message handler and process it asynchronously.
+ *
+ * @author Antoine Bluchet
+ */
+final class DataTransformer implements DataTransformerInterface
+{
+ private $resourceMetadataFactory;
+
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory)
+ {
+ $this->resourceMetadataFactory = $resourceMetadataFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform($object, string $to, array $context = [])
+ {
+ return $object;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsTransformation($data, string $to, array $context = []): bool
+ {
+ if (
+ \is_object($data) // data is not normalized yet, it should be an array
+ ||
+ null === ($context['input']['class'] ?? null)
+ ) {
+ return false;
+ }
+
+ $metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? $to);
+
+ if (!isset($context['operation_type'])) {
+ return 'input' === $metadata->getAttribute('messenger');
+ }
+
+ return 'input' === $metadata->getTypedOperationAttribute(
+ $context['operation_type'],
+ $context[$context['operation_type'].'_operation_name'] ?? '',
+ 'messenger',
+ null,
+ true
+ );
+ }
+}
diff --git a/src/EventListener/RespondListener.php b/src/EventListener/RespondListener.php
index 175be72ff4a..61c98a58157 100644
--- a/src/EventListener/RespondListener.php
+++ b/src/EventListener/RespondListener.php
@@ -45,7 +45,13 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
$controllerResult = $event->getControllerResult();
$request = $event->getRequest();
- if ($controllerResult instanceof Response || !(($attributes = RequestAttributesExtractor::extractAttributes($request))['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) {
+ $attributes = RequestAttributesExtractor::extractAttributes($request);
+ if ($controllerResult instanceof Response && ($attributes['respond'] ?? false)) {
+ $event->setResponse($controllerResult);
+
+ return;
+ }
+ if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) {
return;
}
diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php
index ef2260bc3a8..2f53c20f526 100644
--- a/src/EventListener/WriteListener.php
+++ b/src/EventListener/WriteListener.php
@@ -73,7 +73,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
if (null !== $this->resourceMetadataFactory) {
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
$outputMetadata = $resourceMetadata->getOperationAttribute($attributes, 'output', ['class' => $attributes['resource_class']], true);
- $hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class'];
+ $hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class'] && $controllerResult instanceof $outputMetadata['class'];
}
if ($hasOutput) {
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index e9f1479054e..aec51e49cb9 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -460,6 +460,7 @@ public function testDisabledMessenger()
$containerBuilderProphecy = $this->getBaseContainerBuilderProphecy();
$containerBuilderProphecy->setAlias('api_platform.message_bus', 'message_bus')->shouldNotBeCalled();
$containerBuilderProphecy->setDefinition('api_platform.messenger.data_persister', Argument::type(Definition::class))->shouldNotBeCalled();
+ $containerBuilderProphecy->setDefinition('api_platform.messenger.data_transformer', Argument::type(Definition::class))->shouldNotBeCalled();
$containerBuilder = $containerBuilderProphecy->reveal();
$config = self::DEFAULT_CONFIG;
@@ -973,6 +974,7 @@ private function getBaseContainerBuilderProphecy()
'api_platform.jsonld.normalizer.item.non_resource',
'api_platform.mercure.listener.response.add_link_header',
'api_platform.messenger.data_persister',
+ 'api_platform.messenger.data_transformer',
'api_platform.metadata.extractor.yaml',
'api_platform.metadata.property.metadata_factory.annotation',
'api_platform.metadata.property.metadata_factory.yaml',
diff --git a/tests/Bridge/Symfony/Messenger/DataTransformerTest.php b/tests/Bridge/Symfony/Messenger/DataTransformerTest.php
new file mode 100644
index 00000000000..d876e01b9c5
--- /dev/null
+++ b/tests/Bridge/Symfony/Messenger/DataTransformerTest.php
@@ -0,0 +1,89 @@
+
+ *
+ * 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\Tests\Bridge\Symfony\Messenger;
+
+use ApiPlatform\Core\Api\OperationType;
+use ApiPlatform\Core\Bridge\Symfony\Messenger\DataTransformer;
+use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @author Antoine Bluchet
+ */
+class DataTransformerTest extends TestCase
+{
+ public function testSupport()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['messenger' => 'input']));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertTrue($dataTransformer->supportsTransformation([], Dummy::class, ['input' => ['class' => 'smth']]));
+ }
+
+ public function testSupportWithinRequest()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, ['foo' => ['messenger' => 'input']], null, []));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertTrue($dataTransformer->supportsTransformation([], Dummy::class, ['input' => ['class' => 'smth'], 'operation_type' => OperationType::ITEM, 'item_operation_name' => 'foo']));
+ }
+
+ public function testNoSupport()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['messenger' => true]));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertFalse($dataTransformer->supportsTransformation([], Dummy::class, ['input' => ['class' => 'smth']]));
+ }
+
+ public function testNoSupportWithinRequest()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, ['foo' => ['messenger' => true]], null, []));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertFalse($dataTransformer->supportsTransformation([], Dummy::class, ['input' => ['class' => 'smth'], 'operation_type' => OperationType::ITEM, 'item_operation_name' => 'foo']));
+ }
+
+ public function testNoSupportWithoutInput()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['messenger' => 'input']));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertFalse($dataTransformer->supportsTransformation([], Dummy::class, []));
+ }
+
+ public function testNoSupportWithObject()
+ {
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata(null, null, null, null, null, ['messenger' => 'input']));
+
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertFalse($dataTransformer->supportsTransformation(new Dummy(), Dummy::class, []));
+ }
+
+ public function testTransform()
+ {
+ $dummy = new Dummy();
+ $metadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
+ $dataTransformer = new DataTransformer($metadataFactoryProphecy->reveal());
+ $this->assertSame($dummy, $dataTransformer->transform($dummy, Dummy::class));
+ }
+}
diff --git a/tests/EventListener/RespondListenerTest.php b/tests/EventListener/RespondListenerTest.php
index d2f5d6ca68d..04d73a262ec 100644
--- a/tests/EventListener/RespondListenerTest.php
+++ b/tests/EventListener/RespondListenerTest.php
@@ -180,4 +180,17 @@ public function testSetCustomStatus()
$this->assertSame(Response::HTTP_ACCEPTED, $event->getResponse()->getStatusCode());
}
+
+ public function testHandleResponse()
+ {
+ $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_item_operation_name' => 'get', '_api_respond' => true]);
+ $response = new Response();
+ $eventProphecy = $this->prophesize(GetResponseForControllerResultEvent::class);
+ $eventProphecy->getControllerResult()->willReturn($response);
+ $eventProphecy->getRequest()->willReturn($request);
+ $eventProphecy->setResponse($response)->shouldBeCalled();
+
+ $listener = new RespondListener();
+ $listener->onKernelView($eventProphecy->reveal());
+ }
}
diff --git a/tests/Fixtures/TestBundle/Dto/MessengerInput.php b/tests/Fixtures/TestBundle/Dto/MessengerInput.php
new file mode 100644
index 00000000000..bade50a5e7c
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/MessengerInput.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+class MessengerInput
+{
+ /**
+ * @var string
+ */
+ public $var;
+}
diff --git a/tests/Fixtures/TestBundle/Dto/MessengerResponseInput.php b/tests/Fixtures/TestBundle/Dto/MessengerResponseInput.php
new file mode 100644
index 00000000000..10836ab9a15
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Dto/MessengerResponseInput.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Dto;
+
+class MessengerResponseInput
+{
+ /**
+ * @var string
+ */
+ public $var;
+}
diff --git a/tests/Fixtures/TestBundle/Entity/MessengerWithInput.php b/tests/Fixtures/TestBundle/Entity/MessengerWithInput.php
new file mode 100644
index 00000000000..68a7f224be1
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/MessengerWithInput.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\MessengerInput;
+
+/**
+ * @ApiResource(messenger="input", input=MessengerInput::class)
+ */
+class MessengerWithInput
+{
+ /**
+ * @ApiProperty(identifier=true)
+ */
+ public $id;
+ /**
+ * @var string
+ */
+ public $name;
+}
diff --git a/tests/Fixtures/TestBundle/Entity/MessengerWithResponse.php b/tests/Fixtures/TestBundle/Entity/MessengerWithResponse.php
new file mode 100644
index 00000000000..a8d85d24ab3
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/MessengerWithResponse.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\MessengerResponseInput;
+
+/**
+ * @ApiResource(messenger="input", input=MessengerResponseInput::class)
+ */
+class MessengerWithResponse
+{
+ /**
+ * @ApiProperty(identifier=true)
+ */
+ public $id;
+ /**
+ * @var string
+ */
+ public $name;
+}
diff --git a/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithInputHandler.php b/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithInputHandler.php
new file mode 100644
index 00000000000..3c282c43ae4
--- /dev/null
+++ b/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithInputHandler.php
@@ -0,0 +1,30 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\MessengerHandler;
+
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\MessengerInput;
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\MessengerWithInput;
+use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+
+class MessengerWithInputHandler implements MessageHandlerInterface
+{
+ public function __invoke(MessengerInput $data)
+ {
+ $object = new MessengerWithInput();
+ $object->name = 'test';
+ $object->id = 1;
+
+ return $object;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithResponseHandler.php b/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithResponseHandler.php
new file mode 100644
index 00000000000..1865750d953
--- /dev/null
+++ b/tests/Fixtures/TestBundle/MessengerHandler/MessengerWithResponseHandler.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Tests\Fixtures\TestBundle\MessengerHandler;
+
+use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\MessengerResponseInput;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+
+class MessengerWithResponseHandler implements MessageHandlerInterface
+{
+ public function __invoke(MessengerResponseInput $data)
+ {
+ $response = new Response();
+ $response->setContent(json_encode([
+ 'data' => 123,
+ ]));
+ $response->headers->set('Content-Type', 'application/json');
+
+ return $response;
+ }
+}
diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml
index b551736019d..5413c7a7807 100644
--- a/tests/Fixtures/app/config/config_common.yml
+++ b/tests/Fixtures/app/config/config_common.yml
@@ -238,3 +238,15 @@ services:
public: false
tags:
- { name: 'api_platform.data_transformer' }
+
+ app.messenger_handler.messenger_with_inputs:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\MessengerHandler\MessengerWithInputHandler'
+ public: false
+ tags:
+ - { name: 'messenger.message_handler' }
+
+ app.messenger_handler.messenger_with_response:
+ class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\MessengerHandler\MessengerWithResponseHandler'
+ public: false
+ tags:
+ - { name: 'messenger.message_handler' }