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' }