Skip to content

Commit 425d5d2

Browse files
committed
Fix WriteListener trying to generate IRI for non-resource
1 parent 9ad81f8 commit 425d5d2

File tree

12 files changed

+215
-29
lines changed

12 files changed

+215
-29
lines changed

features/json/input_output.feature

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Feature: JSON DTO input and output
2+
In order to use the API
3+
As a client software developer
4+
I need to be able to use DTOs on my resources as Input or Output objects.
5+
6+
Background:
7+
Given I add "Accept" header equal to "application/json"
8+
And I add "Content-Type" header equal to "application/json"
9+
10+
Scenario: Messenger handler returning output object
11+
And I send a "POST" request to "/users/password_reset_request" with body:
12+
"""
13+
{
14+
"email": "[email protected]"
15+
}
16+
"""
17+
Then the response status code should be 201
18+
And the response should be in JSON
19+
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
20+
And the JSON should be equal to:
21+
"""
22+
{
23+
"emailSentAt": "2019-07-05T15:44:00+00:00"
24+
}
25+
"""

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@
162162

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

168169
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
169170
</service>

src/Bridge/Symfony/Messenger/DataTransformer.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1818

1919
/**
20-
* Transforms an input that implements the InputMessage interface
21-
* to itself. This gives the ability to send the Input to a
20+
* Transforms an Input to itself. This gives the ability to send the Input to a
2221
* message handler and process it asynchronously.
2322
*
2423
* @author Antoine Bluchet <[email protected]>

src/EventListener/WriteListener.php

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
namespace ApiPlatform\Core\EventListener;
1515

1616
use ApiPlatform\Core\Api\IriConverterInterface;
17+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1718
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
1819
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1920
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
2021
use ApiPlatform\Core\Util\RequestAttributesExtractor;
22+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
2123
use Symfony\Component\HttpFoundation\Response;
2224
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
2325

@@ -29,19 +31,20 @@
2931
*/
3032
final class WriteListener
3133
{
34+
use ResourceClassInfoTrait;
3235
use ToggleableOperationAttributeTrait;
3336

3437
public const OPERATION_ATTRIBUTE_KEY = 'write';
3538

3639
private $dataPersister;
3740
private $iriConverter;
38-
private $resourceMetadataFactory;
3941

40-
public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
42+
public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceClassResolverInterface $resourceClassResolver = null)
4143
{
4244
$this->dataPersister = $dataPersister;
4345
$this->iriConverter = $iriConverter;
4446
$this->resourceMetadataFactory = $resourceMetadataFactory;
47+
$this->resourceClassResolver = $resourceClassResolver;
4548
}
4649

4750
/**
@@ -79,21 +82,29 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
7982
$event->setControllerResult($controllerResult);
8083
}
8184

82-
if (null === $this->iriConverter) {
83-
return;
85+
if ($controllerResult instanceof Response) {
86+
break;
8487
}
8588

8689
$hasOutput = true;
87-
if (null !== $this->resourceMetadataFactory) {
90+
if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
8891
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
89-
$outputMetadata = $resourceMetadata->getOperationAttribute($attributes, 'output', ['class' => $attributes['resource_class']], true);
90-
$hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class'] && $controllerResult instanceof $outputMetadata['class'];
92+
$outputMetadata = $resourceMetadata->getOperationAttribute($attributes, 'output', [
93+
'class' => $attributes['resource_class'],
94+
], true);
95+
96+
$hasOutput = \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class'];
97+
}
98+
99+
if (!$hasOutput) {
100+
break;
91101
}
92102

93-
if ($hasOutput) {
103+
if ($this->iriConverter instanceof IriConverterInterface && $this->isResourceClass($this->getObjectClass($controllerResult))) {
94104
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
95105
}
96-
break;
106+
107+
break;
97108
case 'DELETE':
98109
$this->dataPersister->remove($controllerResult);
99110
$event->setControllerResult(null);

src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
1818
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
1919
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
2021
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
2122

2223
/**
@@ -27,10 +28,10 @@
2728
*/
2829
final class SerializerPropertyMetadataFactory implements PropertyMetadataFactoryInterface
2930
{
30-
private $resourceMetadataFactory;
31+
use ResourceClassInfoTrait;
32+
3133
private $serializerClassMetadataFactory;
3234
private $decorated;
33-
private $resourceClassResolver;
3435

3536
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, PropertyMetadataFactoryInterface $decorated, ResourceClassResolverInterface $resourceClassResolver = null)
3637
{
@@ -212,19 +213,4 @@ private function getClassSerializerGroups(string $class): array
212213

213214
return array_unique($groups);
214215
}
215-
216-
private function isResourceClass(string $class): bool
217-
{
218-
if (null !== $this->resourceClassResolver) {
219-
return $this->resourceClassResolver->isResourceClass($class);
220-
}
221-
222-
try {
223-
$this->resourceMetadataFactory->create($class);
224-
225-
return true;
226-
} catch (ResourceClassNotFoundException $e) {
227-
return false;
228-
}
229-
}
230216
}

src/Util/ResourceClassInfoTrait.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\Util;
1515

1616
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
17+
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1719

1820
/**
1921
* Retrieves information about a resource class.
@@ -29,6 +31,11 @@ trait ResourceClassInfoTrait
2931
*/
3032
private $resourceClassResolver;
3133

34+
/**
35+
* @var ResourceMetadataFactoryInterface|null
36+
*/
37+
private $resourceMetadataFactory;
38+
3239
/**
3340
* Gets the resource class of the given object.
3441
*
@@ -51,4 +58,24 @@ private function getResourceClass($object, bool $strict = false): ?string
5158

5259
return $this->resourceClassResolver->getResourceClass($object);
5360
}
61+
62+
private function isResourceClass(string $class): bool
63+
{
64+
if ($this->resourceClassResolver instanceof ResourceClassResolverInterface) {
65+
return $this->resourceClassResolver->isResourceClass($class);
66+
}
67+
68+
if (!$this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
69+
// assume that it's a resource class
70+
return true;
71+
}
72+
73+
try {
74+
$this->resourceMetadataFactory->create($class);
75+
} catch (ResourceClassNotFoundException $e) {
76+
return false;
77+
}
78+
79+
return true;
80+
}
5481
}

tests/Fixtures/TestBundle/Document/User.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document;
1515

1616
use ApiPlatform\Core\Annotation\ApiResource;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequest;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult;
1719
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
1820
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
1921
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
@@ -30,6 +32,23 @@
3032
* "normalization_context"={"groups"={"user", "user-read"}},
3133
* "denormalization_context"={"groups"={"user", "user-write"}}
3234
* },
35+
* collectionOperations={
36+
* "post",
37+
* "get",
38+
* "post_password_reset_request"={
39+
* "method"="POST",
40+
* "path"="/users/password_reset_request",
41+
* "messenger"="input",
42+
* "input"=PasswordResetRequest::class,
43+
* "output"=PasswordResetRequestResult::class,
44+
* "normalization_context"={
45+
* "groups"={"user_password_reset_request"},
46+
* },
47+
* "denormalization_context"={
48+
* "groups"={"user_password_reset_request"},
49+
* },
50+
* },
51+
* },
3352
* itemOperations={"get", "put", "delete",
3453
* "recover_password"={
3554
* "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto;
15+
16+
use Symfony\Component\Serializer\Annotation\Groups;
17+
18+
final class PasswordResetRequest
19+
{
20+
/**
21+
* @Groups({"user_password_reset_request"})
22+
*/
23+
private $email;
24+
25+
public function __construct(string $email = '')
26+
{
27+
$this->email = $email;
28+
}
29+
30+
public function getEmail(): string
31+
{
32+
return $this->email;
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto;
15+
16+
use Symfony\Component\Serializer\Annotation\Groups;
17+
18+
final class PasswordResetRequestResult
19+
{
20+
/**
21+
* @Groups({"user_password_reset_request"})
22+
*/
23+
private $emailSentAt;
24+
25+
public function __construct(\DateTimeInterface $emailSentAt)
26+
{
27+
$this->emailSentAt = $emailSentAt;
28+
}
29+
30+
public function getEmailSentAt(): \DateTimeInterface
31+
{
32+
return $this->emailSentAt;
33+
}
34+
}

tests/Fixtures/TestBundle/Entity/User.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
1515

1616
use ApiPlatform\Core\Annotation\ApiResource;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequest;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\PasswordResetRequestResult;
1719
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordInput;
1820
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\RecoverPasswordOutput;
1921
use Doctrine\ORM\Mapping as ORM;
@@ -31,6 +33,23 @@
3133
* "normalization_context"={"groups"={"user", "user-read"}},
3234
* "denormalization_context"={"groups"={"user", "user-write"}}
3335
* },
36+
* collectionOperations={
37+
* "post",
38+
* "get",
39+
* "post_password_reset_request"={
40+
* "method"="POST",
41+
* "path"="/users/password_reset_request",
42+
* "messenger"="input",
43+
* "input"=PasswordResetRequest::class,
44+
* "output"=PasswordResetRequestResult::class,
45+
* "normalization_context"={
46+
* "groups"={"user_password_reset_request"},
47+
* },
48+
* "denormalization_context"={
49+
* "groups"={"user_password_reset_request"},
50+
* },
51+
* },
52+
* },
3453
* itemOperations={"get", "put", "delete",
3554
* "recover_password"={
3655
* "input"=RecoverPasswordInput::class, "output"=RecoverPasswordOutput::class, "method"="PUT", "path"="users/recover/{id}"

0 commit comments

Comments
 (0)