Skip to content

Commit

Permalink
Fix WriteListener trying to generate IRI for non-resource
Browse files Browse the repository at this point in the history
  • Loading branch information
teohhanhui committed Jul 5, 2019
1 parent 9ad81f8 commit 425d5d2
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 29 deletions.
25 changes: 25 additions & 0 deletions features/json/input_output.feature
Original file line number Diff line number Diff line change
@@ -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"
}
"""
3 changes: 2 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@

<service id="api_platform.listener.view.write" class="ApiPlatform\Core\EventListener\WriteListener">
<argument type="service" id="api_platform.data_persister" />
<argument type="service" id="api_platform.iri_converter" on-invalid="null" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.resource_class_resolver" />

<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
</service>
Expand Down
3 changes: 1 addition & 2 deletions src/Bridge/Symfony/Messenger/DataTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <soyuka@gmail.com>
Expand Down
29 changes: 20 additions & 9 deletions src/EventListener/WriteListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}
}
}
27 changes: 27 additions & 0 deletions src/Util/ResourceClassInfoTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -29,6 +31,11 @@ trait ResourceClassInfoTrait
*/
private $resourceClassResolver;

/**
* @var ResourceMetadataFactoryInterface|null
*/
private $resourceMetadataFactory;

/**
* Gets the resource class of the given object.
*
Expand All @@ -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;
}
}
19 changes: 19 additions & 0 deletions tests/Fixtures/TestBundle/Document/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}"
Expand Down
34 changes: 34 additions & 0 deletions tests/Fixtures/TestBundle/Dto/PasswordResetRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
}
}
34 changes: 34 additions & 0 deletions tests/Fixtures/TestBundle/Dto/PasswordResetRequestResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;
}
}
19 changes: 19 additions & 0 deletions tests/Fixtures/TestBundle/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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'));
}
}
5 changes: 5 additions & 0 deletions tests/Fixtures/app/config/config_common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 425d5d2

Please sign in to comment.