Skip to content

Commit 89dfb20

Browse files
committed
Improve Input/Output with normalizers
1 parent 1a9335e commit 89dfb20

25 files changed

+745
-152
lines changed

features/bootstrap/DoctrineContext.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
6060
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
6161
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDate;
62+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
6263
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
6364
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
6465
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
@@ -1182,6 +1183,36 @@ public function thereIsAMaxDepthDummyWithLevelOfDescendants(int $level)
11821183
$this->manager->flush();
11831184
}
11841185

1186+
/**
1187+
* @Given there is a DummyCustomDto
1188+
*/
1189+
public function thereIsADummyCustomDto()
1190+
{
1191+
$dto = new DummyDtoCustom();
1192+
$dto->lorem = 'test';
1193+
$dto->ipsum = '0';
1194+
$this->manager->persist($dto);
1195+
1196+
$this->manager->flush();
1197+
$this->manager->clear();
1198+
}
1199+
1200+
/**
1201+
* @Given there are :nb DummyCustomDto
1202+
*/
1203+
public function thereAreNbDummyCustomDto($nb)
1204+
{
1205+
for ($i = 1; $i <= $nb; ++$i) {
1206+
$dto = new DummyDtoCustom();
1207+
$dto->lorem = 'test';
1208+
$dto->ipsum = (string) $i;
1209+
$this->manager->persist($dto);
1210+
}
1211+
1212+
$this->manager->flush();
1213+
$this->manager->clear();
1214+
}
1215+
11851216
private function isOrm(): bool
11861217
{
11871218
return null !== $this->schemaTool;

features/main/dto.feature

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
Feature: DTO input and output
2+
In order to use an hypermedia 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+
@createSchema
7+
Scenario: Create a resource with a custom Input.
8+
When I add "Content-Type" header equal to "application/ld+json"
9+
And I send a "POST" request to "/dummy_dto_customs" with body:
10+
"""
11+
{
12+
"foo": "test",
13+
"bar": 1
14+
}
15+
"""
16+
Then the response status code should be 201
17+
And the response should be in JSON
18+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the JSON should be equal to:
20+
"""
21+
{
22+
"@context": "/contexts/DummyDtoCustom",
23+
"@id": "/dummy_dto_customs/1",
24+
"@type": "DummyDtoCustom",
25+
"lorem": "test",
26+
"ipsum": "1",
27+
"id": 1
28+
}
29+
"""
30+
31+
@createSchema
32+
Scenario: Get an item with a custom output
33+
Given there is a DummyCustomDto
34+
When I send a "GET" request to "/dummy_dto_custom_output/1"
35+
Then the response status code should be 200
36+
And the response should be in JSON
37+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
38+
And the JSON should be a superset of:
39+
"""
40+
{
41+
"@context": "/contexts/CustomOutputDto",
42+
"@type": "CustomOutputDto",
43+
"foo": "test",
44+
"bar": 0
45+
}
46+
"""
47+
48+
@createSchema
49+
Scenario: Get a collection with a custom output
50+
Given there are 2 DummyCustomDto
51+
When I send a "GET" request to "/dummy_dto_custom_output"
52+
Then the response status code should be 200
53+
And the response should be in JSON
54+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
55+
And the JSON should be a superset of:
56+
"""
57+
{
58+
"@context": "/contexts/DummyDtoCustom",
59+
"@id": "/dummy_dto_customs",
60+
"@type": "hydra:Collection",
61+
"hydra:member": [
62+
{
63+
"@type": "CustomOutputDto",
64+
"foo": "test",
65+
"bar": 1
66+
},
67+
{
68+
"@type": "CustomOutputDto",
69+
"foo": "test",
70+
"bar": 2
71+
}
72+
],
73+
"hydra:totalItems": 2
74+
}
75+
"""
76+
77+
@createSchema
78+
Scenario: Create a DummyCustomDto object without output
79+
When I add "Content-Type" header equal to "application/ld+json"
80+
And I send a "POST" request to "/dummy_dto_custom_post_without_output" with body:
81+
"""
82+
{
83+
"lorem": "test",
84+
"ipsum": "1"
85+
}
86+
"""
87+
Then the response status code should be 201
88+
And the response should be empty
89+
90+
@createSchema
91+
Scenario: Create and update a DummyInputOutput
92+
When I add "Content-Type" header equal to "application/ld+json"
93+
And I send a "POST" request to "/dummy_dto_input_outputs" with body:
94+
"""
95+
{
96+
"foo": "test",
97+
"bar": 1
98+
}
99+
"""
100+
Then the response status code should be 201
101+
And the JSON should be a superset of:
102+
"""
103+
{
104+
"@context": "/contexts/OutputDto",
105+
"@type": "OutputDto",
106+
"baz": 1,
107+
"bat": "test"
108+
}
109+
"""
110+
Then I add "Content-Type" header equal to "application/ld+json"
111+
And I send a "PUT" request to "/dummy_dto_input_outputs/1" with body:
112+
"""
113+
{
114+
"foo": "test",
115+
"bar": 2
116+
}
117+
"""
118+
Then the response status code should be 200
119+
And the JSON should be a superset of:
120+
"""
121+
{
122+
"@context": "/contexts/OutputDto",
123+
"@type": "OutputDto",
124+
"baz": 2,
125+
"bat": "test"
126+
}
127+
"""
128+
129+
@createSchema
130+
Scenario: Use DTO with relations on User
131+
When I add "Content-Type" header equal to "application/ld+json"
132+
And I send a "POST" request to "/users" with body:
133+
"""
134+
{
135+
"username": "soyuka",
136+
"plainPassword": "a real password",
137+
"email": "soyuka@example.com"
138+
}
139+
"""
140+
Then the response status code should be 201
141+
Then I add "Content-Type" header equal to "application/ld+json"
142+
And I send a "PUT" request to "/users/recover/1" with body:
143+
"""
144+
{
145+
"user": "/users/1"
146+
}
147+
"""
148+
Then the response status code should be 200
149+
And the JSON should be a superset of:
150+
"""
151+
{
152+
"@context": "/contexts/RecoverPasswordOutput",
153+
"@type": "RecoverPasswordOutput",
154+
"user": {
155+
"@id": "/users/1",
156+
"@type": "User",
157+
"email": "soyuka@example.com",
158+
"fullname": null,
159+
"username": "soyuka"
160+
}
161+
}
162+
"""
163+
164+
# @createSchema
165+
# Scenario: Execute a GraphQL query on DTO
166+
# Given there are 2 DummyCustomDto
167+
# When I send the following GraphQL request:
168+
# """
169+
# {
170+
# dummyDtoCustom(id: "/dummy_dto_customs/1") {
171+
# lorem
172+
# ipsum
173+
# }
174+
# }
175+
# """
176+
# Then the response status code should be 200
177+
# And the response should be in JSON
178+
# And the header "Content-Type" should be equal to "application/json"
179+
# Then print last JSON response
180+
# And the JSON node "data.dummy.id" should be equal to "/dummies/1"
181+
# And the JSON node "data.dummy.name" should be equal to "Dummy #1"

src/Api/ResourceClassResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function getResourceClass($value, string $resourceClass = null, bool $str
6161
return $type;
6262
}
6363

64-
throw new InvalidArgumentException(sprintf('No resource class found for object of type "%s".', $type));
64+
return $type;
6565
}
6666

6767
/**

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ public function getItemFromIri(string $iri, array $context = [])
116116
public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string
117117
{
118118
$resourceClass = $this->getObjectClass($item);
119-
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);
119+
120+
try {
121+
$routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM);
122+
} catch (InvalidArgumentException $e) {
123+
return '_:'.md5(serialize($item));
124+
}
120125

121126
try {
122127
$identifiers = $this->generateIdentifiersUrl($this->identifiersExtractor->getIdentifiersFromItem($item), $resourceClass);

src/EventListener/DeserializeListener.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function onKernelRequest(GetResponseEvent $event)
7878
}
7979

8080
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
81-
if (false === $context['input_class']) {
81+
if (false === ($context['input_class'] ?? null)) {
8282
return;
8383
}
8484

@@ -97,7 +97,7 @@ public function onKernelRequest(GetResponseEvent $event)
9797
$request->attributes->set(
9898
'data',
9999
$this->serializer->deserialize(
100-
$requestContent, $context['input_class'], $format, $context
100+
$requestContent, $context['resource_class'], $format, $context
101101
)
102102
);
103103
}

src/EventListener/SerializeListener.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
6262
$request->attributes->set('_api_respond', true);
6363
$context = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
6464

65-
if (isset($context['output_class'])) {
66-
if (false === $context['output_class']) {
67-
// If the output class is explicitly set to false, the response must be empty
68-
$event->setControllerResult('');
65+
if (false === ($context['output_class'] ?? null)) {
66+
$event->setControllerResult('');
6967

70-
return;
71-
}
72-
73-
$context['resource_class'] = $context['output_class'];
68+
return;
7469
}
7570

7671
if ($included = $request->attributes->get('_api_included')) {

src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ private function handleNotFound(ResourceMetadata $parentPropertyMetadata = null,
7373
return $parentPropertyMetadata;
7474
}
7575

76+
if (false !== $pos = strrpos($resourceClass, '\\')) {
77+
return new ResourceMetadata(substr($resourceClass, $pos + 1));
78+
}
79+
7680
throw new ResourceClassNotFoundException(sprintf('Resource "%s" not found.', $resourceClass));
7781
}
7882

src/Serializer/AbstractItemNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function supportsNormalization($data, $format = null)
8686
return false;
8787
}
8888

89-
return $this->resourceClassResolver->isResourceClass($this->getObjectClass($data));
89+
return true;
9090
}
9191

9292
/**
@@ -119,7 +119,7 @@ public function normalize($object, $format = null, array $context = [])
119119
*/
120120
public function supportsDenormalization($data, $type, $format = null)
121121
{
122-
return $this->localCache[$type] ?? $this->localCache[$type] = $this->resourceClassResolver->isResourceClass($type);
122+
return true;
123123
}
124124

125125
/**

tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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+
class CustomInputDto
17+
{
18+
/**
19+
* @var string
20+
*/
21+
public $foo;
22+
23+
/**
24+
* @var int
25+
*/
26+
public $bar;
27+
}

0 commit comments

Comments
 (0)