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