From 425d5d2b317c076caa435546b3c887f547e210de Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Fri, 5 Jul 2019 17:25:07 +0200 Subject: [PATCH] Fix WriteListener trying to generate IRI for non-resource --- features/json/input_output.feature | 25 ++++++++++++++ .../Symfony/Bundle/Resources/config/api.xml | 3 +- .../Symfony/Messenger/DataTransformer.php | 3 +- src/EventListener/WriteListener.php | 29 +++++++++++----- .../SerializerPropertyMetadataFactory.php | 20 ++--------- src/Util/ResourceClassInfoTrait.php | 27 +++++++++++++++ tests/Fixtures/TestBundle/Document/User.php | 19 +++++++++++ .../TestBundle/Dto/PasswordResetRequest.php | 34 +++++++++++++++++++ .../Dto/PasswordResetRequestResult.php | 34 +++++++++++++++++++ tests/Fixtures/TestBundle/Entity/User.php | 19 +++++++++++ .../PasswordResetRequestHandler.php | 26 ++++++++++++++ tests/Fixtures/app/config/config_common.yml | 5 +++ 12 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 features/json/input_output.feature create mode 100644 tests/Fixtures/TestBundle/Dto/PasswordResetRequest.php create mode 100644 tests/Fixtures/TestBundle/Dto/PasswordResetRequestResult.php create mode 100644 tests/Fixtures/TestBundle/MessageHandler/PasswordResetRequestHandler.php diff --git a/features/json/input_output.feature b/features/json/input_output.feature new file mode 100644 index 00000000000..032922cacbd --- /dev/null +++ b/features/json/input_output.feature @@ -0,0 +1,25 @@ +Feature: JSON DTO input and output + In order to use the API + As a client software developer + I need to be able to use DTOs on my resources as Input or Output objects. + + Background: + Given I add "Accept" header equal to "application/json" + And I add "Content-Type" header equal to "application/json" + + Scenario: Messenger handler returning output object + And I send a "POST" request to "/users/password_reset_request" with body: + """ + { + "email": "user@example.com" + } + """ + 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/json; charset=utf-8" + And the JSON should be equal to: + """ + { + "emailSentAt": "2019-07-05T15:44:00+00:00" + } + """ diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 67473b6fc91..3b484b60b36 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -162,8 +162,9 @@ - + + diff --git a/src/Bridge/Symfony/Messenger/DataTransformer.php b/src/Bridge/Symfony/Messenger/DataTransformer.php index 8fcf9fe51b0..dac49c37d21 100644 --- a/src/Bridge/Symfony/Messenger/DataTransformer.php +++ b/src/Bridge/Symfony/Messenger/DataTransformer.php @@ -17,8 +17,7 @@ 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 + * Transforms an Input to itself. This gives the ability to send the Input to a * message handler and process it asynchronously. * * @author Antoine Bluchet diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index 26ae4829baa..16042d735c0 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -14,10 +14,12 @@ namespace ApiPlatform\Core\EventListener; use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\DataPersister\DataPersisterInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait; use ApiPlatform\Core\Util\RequestAttributesExtractor; +use ApiPlatform\Core\Util\ResourceClassInfoTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; @@ -29,19 +31,20 @@ */ final class WriteListener { + use ResourceClassInfoTrait; use ToggleableOperationAttributeTrait; public const OPERATION_ATTRIBUTE_KEY = 'write'; private $dataPersister; private $iriConverter; - private $resourceMetadataFactory; - public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceClassResolverInterface $resourceClassResolver = null) { $this->dataPersister = $dataPersister; $this->iriConverter = $iriConverter; $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; } /** @@ -79,21 +82,29 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void $event->setControllerResult($controllerResult); } - if (null === $this->iriConverter) { - return; + if ($controllerResult instanceof Response) { + break; } $hasOutput = true; - if (null !== $this->resourceMetadataFactory) { + if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { $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'] && $controllerResult instanceof $outputMetadata['class']; + $outputMetadata = $resourceMetadata->getOperationAttribute($attributes, 'output', [ + 'class' => $attributes['resource_class'], + ], true); + + $hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class']; + } + + if (!$hasOutput) { + break; } - if ($hasOutput) { + if ($this->iriConverter instanceof IriConverterInterface && $this->isResourceClass($this->getObjectClass($controllerResult))) { $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult)); } - break; + + break; case 'DELETE': $this->dataPersister->remove($controllerResult); $event->setControllerResult(null); diff --git a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php index 0e5b900f124..dd165208b26 100644 --- a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php @@ -17,6 +17,7 @@ use ApiPlatform\Core\Exception\ResourceClassNotFoundException; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Util\ResourceClassInfoTrait; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface; /** @@ -27,10 +28,10 @@ */ final class SerializerPropertyMetadataFactory implements PropertyMetadataFactoryInterface { - private $resourceMetadataFactory; + use ResourceClassInfoTrait; + private $serializerClassMetadataFactory; private $decorated; - private $resourceClassResolver; public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, PropertyMetadataFactoryInterface $decorated, ResourceClassResolverInterface $resourceClassResolver = null) { @@ -212,19 +213,4 @@ private function getClassSerializerGroups(string $class): array return array_unique($groups); } - - private function isResourceClass(string $class): bool - { - if (null !== $this->resourceClassResolver) { - return $this->resourceClassResolver->isResourceClass($class); - } - - try { - $this->resourceMetadataFactory->create($class); - - return true; - } catch (ResourceClassNotFoundException $e) { - return false; - } - } } diff --git a/src/Util/ResourceClassInfoTrait.php b/src/Util/ResourceClassInfoTrait.php index 9d64bf561ef..fda94b0bc8f 100644 --- a/src/Util/ResourceClassInfoTrait.php +++ b/src/Util/ResourceClassInfoTrait.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Core\Util; use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; /** * Retrieves information about a resource class. @@ -29,6 +31,11 @@ trait ResourceClassInfoTrait */ private $resourceClassResolver; + /** + * @var ResourceMetadataFactoryInterface|null + */ + private $resourceMetadataFactory; + /** * Gets the resource class of the given object. * @@ -51,4 +58,24 @@ private function getResourceClass($object, bool $strict = false): ?string return $this->resourceClassResolver->getResourceClass($object); } + + private function isResourceClass(string $class): bool + { + if ($this->resourceClassResolver instanceof ResourceClassResolverInterface) { + return $this->resourceClassResolver->isResourceClass($class); + } + + if (!$this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { + // assume that it's a resource class + return true; + } + + try { + $this->resourceMetadataFactory->create($class); + } catch (ResourceClassNotFoundException $e) { + return false; + } + + return true; + } } diff --git a/tests/Fixtures/TestBundle/Document/User.php b/tests/Fixtures/TestBundle/Document/User.php index 9d559b87218..ca3abe9a1a1 100644 --- a/tests/Fixtures/TestBundle/Document/User.php +++ b/tests/Fixtures/TestBundle/Document/User.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequest; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; @@ -30,6 +32,23 @@ * "normalization_context"={"groups"={"user", "user-read"}}, * "denormalization_context"={"groups"={"user", "user-write"}} * }, + * collectionOperations={ + * "post", + * "get", + * "post_password_reset_request"={ + * "method"="POST", + * "path"="/users/password_reset_request", + * "messenger"="input", + * "input"=PasswordResetRequest::class, + * "output"=PasswordResetRequestResult::class, + * "normalization_context"={ + * "groups"={"user_password_reset_request"}, + * }, + * "denormalization_context"={ + * "groups"={"user_password_reset_request"}, + * }, + * }, + * }, * itemOperations={"get", "put", "delete", * "recover_password"={ * "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}" diff --git a/tests/Fixtures/TestBundle/Dto/PasswordResetRequest.php b/tests/Fixtures/TestBundle/Dto/PasswordResetRequest.php new file mode 100644 index 00000000000..3bc58140fa5 --- /dev/null +++ b/tests/Fixtures/TestBundle/Dto/PasswordResetRequest.php @@ -0,0 +1,34 @@ + + * + * 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; + +use Symfony\Component\Serializer\Annotation\Groups; + +final class PasswordResetRequest +{ + /** + * @Groups({"user_password_reset_request"}) + */ + private $email; + + public function __construct(string $email = '') + { + $this->email = $email; + } + + public function getEmail(): string + { + return $this->email; + } +} diff --git a/tests/Fixtures/TestBundle/Dto/PasswordResetRequestResult.php b/tests/Fixtures/TestBundle/Dto/PasswordResetRequestResult.php new file mode 100644 index 00000000000..1f4b3786177 --- /dev/null +++ b/tests/Fixtures/TestBundle/Dto/PasswordResetRequestResult.php @@ -0,0 +1,34 @@ + + * + * 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; + +use Symfony\Component\Serializer\Annotation\Groups; + +final class PasswordResetRequestResult +{ + /** + * @Groups({"user_password_reset_request"}) + */ + private $emailSentAt; + + public function __construct(\DateTimeInterface $emailSentAt) + { + $this->emailSentAt = $emailSentAt; + } + + public function getEmailSentAt(): \DateTimeInterface + { + return $this->emailSentAt; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/User.php b/tests/Fixtures/TestBundle/Entity/User.php index 89ca2aa3b07..5a6b0261926 100644 --- a/tests/Fixtures/TestBundle/Entity/User.php +++ b/tests/Fixtures/TestBundle/Entity/User.php @@ -14,6 +14,8 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequest; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput; use Doctrine\ORM\Mapping as ORM; @@ -31,6 +33,23 @@ * "normalization_context"={"groups"={"user", "user-read"}}, * "denormalization_context"={"groups"={"user", "user-write"}} * }, + * collectionOperations={ + * "post", + * "get", + * "post_password_reset_request"={ + * "method"="POST", + * "path"="/users/password_reset_request", + * "messenger"="input", + * "input"=PasswordResetRequest::class, + * "output"=PasswordResetRequestResult::class, + * "normalization_context"={ + * "groups"={"user_password_reset_request"}, + * }, + * "denormalization_context"={ + * "groups"={"user_password_reset_request"}, + * }, + * }, + * }, * itemOperations={"get", "put", "delete", * "recover_password"={ * "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}" diff --git a/tests/Fixtures/TestBundle/MessageHandler/PasswordResetRequestHandler.php b/tests/Fixtures/TestBundle/MessageHandler/PasswordResetRequestHandler.php new file mode 100644 index 00000000000..881fca688f3 --- /dev/null +++ b/tests/Fixtures/TestBundle/MessageHandler/PasswordResetRequestHandler.php @@ -0,0 +1,26 @@ + + * + * 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\MessageHandler; + +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequest; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +class PasswordResetRequestHandler implements MessageHandlerInterface +{ + public function __invoke(PasswordResetRequest $passwordResetRequest): PasswordResetRequestResult + { + return new PasswordResetRequestResult(new \DateTimeImmutable('2019-07-05T15:44:00Z')); + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 17b090aeb39..c6d853d9632 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -84,6 +84,11 @@ parameters: container.dumper.inline_class_loader: true services: + ApiPlatform\Core\Tests\Fixtures\TestBundle\MessageHandler\: + resource: '../../TestBundle/MessageHandler' + autowire: true + autoconfigure: true + contain_non_resource.item_data_provider: class: 'ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\ContainNonResourceItemDataProvider' public: false