Skip to content

Commit 3408ca9

Browse files
committed
Improve Input/Output with normalizers
1 parent 1a9335e commit 3408ca9

25 files changed

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