Skip to content

Commit d6e9f57

Browse files
committed
Merge 3.4
2 parents fd50f8b + cb3aa86 commit d6e9f57

File tree

10 files changed

+180
-67
lines changed

10 files changed

+180
-67
lines changed

features/main/exception_to_status.feature

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ Feature: Using exception_to_status config
4545
And I send a "GET" request to "/issue5924"
4646
Then the response status code should be 429
4747
Then the header "retry-after" should be equal to 32
48+
49+
Scenario: Show error page
50+
When I add "Accept" header equal to "text/html"
51+
And I send a "GET" request to "/errors/404"
52+
Then the response status code should be 200

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface SerializerContextBuilderInterface
5050
* exclude_from_cache_key?: string[],
5151
* api_included?: bool,
5252
* attributes?: string[],
53+
* deserializer_type?: string,
5354
* }
5455
*/
5556
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": "^11.2",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Symfony\Action;
15+
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19+
20+
final class ErrorPageAction
21+
{
22+
public function __invoke(Request $request): Response
23+
{
24+
$status = $request->attributes->get('status');
25+
$text = Response::$statusTexts[$status] ?? throw new NotFoundHttpException();
26+
27+
return new Response(<<<HTML
28+
<!DOCTYPE html>
29+
<html>
30+
<head>
31+
<meta charset="UTF-8" />
32+
<title>Error $status</title>
33+
</head>
34+
<body><h1>Error $status</h1>$text</body>
35+
</html>
36+
HTML);
37+
}
38+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<service id="api_platform.property_accessor" alias="property_accessor" public="false" />
1414
<service id="api_platform.property_info" alias="property_info" public="false" />
1515
<service id="api_platform.negotiator" class="Negotiation\Negotiator" public="false" />
16+
<service id="api_platform.action.error_page" class="ApiPlatform\Symfony\Action\ErrorPageAction" public="true" />
1617

1718
<service id="api_platform.resource_class_resolver" class="ApiPlatform\Metadata\ResourceClassResolver" public="false">
1819
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />

src/Symfony/Bundle/Resources/config/routing/errors.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
http://symfony.com/schema/routing/routing-1.0.xsd">
77

88
<route id="api_errors" path="/errors/{status}" methods="GET|HEAD">
9-
<default key="_controller">api_platform.action.not_exposed</default>
10-
<default key="status">500</default>
9+
<default key="_controller">api_platform.action.error_page</default>
1110

1211
<requirement key="status">\d+</requirement>
1312
</route>

tests/State/Provider/DeserializeProviderTest.php

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

0 commit comments

Comments
 (0)