Skip to content

Commit e91c783

Browse files
authored
feat(state): "deserializer_type" context (#6429)
1 parent 26d700e commit e91c783

File tree

6 files changed

+136
-66
lines changed

6 files changed

+136
-66
lines changed

src/Metadata/Exception/UnsupportedMediaTypeHttpException.php

Whitespace-only changes.

src/State/Provider/DeserializeProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
9090
}
9191

9292
try {
93-
return $this->serializer->deserialize((string) $request->getContent(), $operation->getClass(), $format, $serializerContext);
93+
return $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext);
9494
} catch (PartialDenormalizationException $e) {
9595
if (!class_exists(ConstraintViolationList::class)) {
9696
throw $e;

src/State/SerializerContextBuilderInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ interface SerializerContextBuilderInterface
4848
* deep_object_to_populate?: bool,
4949
* collect_denormalization_errors?: bool,
5050
* exclude_from_cache_key?: string[],
51-
* api_included?: bool
51+
* api_included?: bool,
52+
* deserializer_type?: bool,
5253
* }
5354
*/
5455
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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\State\Tests\Provider;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\Metadata\Post;
18+
use ApiPlatform\State\Provider\DeserializeProvider;
19+
use ApiPlatform\State\ProviderInterface;
20+
use ApiPlatform\State\SerializerContextBuilderInterface;
21+
use PHPUnit\Framework\TestCase;
22+
use Symfony\Component\HttpFoundation\Request;
23+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
24+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
25+
use Symfony\Component\Serializer\SerializerInterface;
26+
27+
class DeserializeProviderTest extends TestCase
28+
{
29+
public function testDeserialize(): void
30+
{
31+
$objectToPopulate = new \stdClass();
32+
$serializerContext = [];
33+
$operation = new Post(deserialize: true, class: 'Test');
34+
$decorated = $this->createStub(ProviderInterface::class);
35+
$decorated->method('provide')->willReturn($objectToPopulate);
36+
37+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
38+
$serializerContextBuilder->expects($this->once())->method('createFromRequest')->willReturn($serializerContext);
39+
$serializer = $this->createMock(SerializerInterface::class);
40+
$serializer->expects($this->once())->method('deserialize')->with('test', 'Test', 'format', ['uri_variables' => ['id' => 1], AbstractNormalizer::OBJECT_TO_POPULATE => $objectToPopulate] + $serializerContext)->willReturn(new \stdClass());
41+
42+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
43+
$request = new Request(content: 'test');
44+
$request->headers->set('CONTENT_TYPE', 'ok');
45+
$request->attributes->set('input_format', 'format');
46+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
47+
}
48+
49+
public function testDeserializeNoContentType(): void
50+
{
51+
$this->expectException(UnsupportedMediaTypeHttpException::class);
52+
$operation = new Get(deserialize: true, class: 'Test');
53+
$decorated = $this->createStub(ProviderInterface::class);
54+
$decorated->method('provide')->willReturn(null);
55+
56+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
57+
$serializer = $this->createMock(SerializerInterface::class);
58+
59+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
60+
$request = new Request(content: 'test');
61+
$request->attributes->set('input_format', 'format');
62+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
63+
}
64+
65+
public function testDeserializeNoInput(): void
66+
{
67+
$this->expectException(UnsupportedMediaTypeHttpException::class);
68+
$operation = new Get(deserialize: true, class: 'Test');
69+
$decorated = $this->createStub(ProviderInterface::class);
70+
$decorated->method('provide')->willReturn(null);
71+
72+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
73+
$serializer = $this->createMock(SerializerInterface::class);
74+
75+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
76+
$request = new Request(content: 'test');
77+
$request->headers->set('CONTENT_TYPE', 'ok');
78+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
79+
}
80+
81+
public function testDeserializeWithContextClass(): void
82+
{
83+
$serializerContext = ['deserializer_type' => 'Test'];
84+
$operation = new Get(deserialize: true);
85+
$decorated = $this->createStub(ProviderInterface::class);
86+
$decorated->method('provide')->willReturn(null);
87+
88+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
89+
$serializerContextBuilder->expects($this->once())->method('createFromRequest')->willReturn($serializerContext);
90+
$serializer = $this->createMock(SerializerInterface::class);
91+
$serializer->expects($this->once())->method('deserialize')->with('test', 'Test', 'format', ['uri_variables' => ['id' => 1]] + $serializerContext)->willReturn(new \stdClass());
92+
93+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
94+
$request = new Request(content: 'test');
95+
$request->headers->set('CONTENT_TYPE', 'ok');
96+
$request->attributes->set('input_format', 'format');
97+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
98+
}
99+
100+
public function testRequestWithEmptyContentType(): void
101+
{
102+
$expectedResult = new \stdClass();
103+
$decorated = $this->createMock(ProviderInterface::class);
104+
$decorated->method('provide')->willReturn($expectedResult);
105+
106+
$serializer = $this->createStub(SerializerInterface::class);
107+
$serializerContextBuilder = $this->createStub(SerializerContextBuilderInterface::class);
108+
109+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
110+
111+
// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
112+
// without a content-type and content-length header will result in the
113+
// variables set to an empty string, not null
114+
115+
$request = new Request(
116+
server: [
117+
'REQUEST_METHOD' => 'POST',
118+
'REQUEST_URI' => '/',
119+
'CONTENT_TYPE' => '',
120+
'CONTENT_LENGTH' => '',
121+
],
122+
content: ''
123+
);
124+
125+
$operation = new Post(deserialize: true);
126+
$context = ['request' => $request];
127+
128+
$this->expectException(UnsupportedMediaTypeHttpException::class);
129+
$provider->provide($operation, [], $context);
130+
}
131+
}

src/State/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"require": {
3030
"php": ">=8.1",
3131
"api-platform/metadata": "^3.4 || ^4.0",
32-
"psr/container": "^1.0 || ^2.0"
32+
"psr/container": "^1.0 || ^2.0",
33+
"symfony/http-kernel": "^6.4 || 7.0"
3334
},
3435
"require-dev": {
3536
"phpunit/phpunit": "^10.3",

tests/State/Provider/DeserializeProviderTest.php

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)