diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php
index 96b6d0ee98e14..a64da93b382fb 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
+use Composer\InstalledVersions;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -23,13 +24,14 @@ class ApiAttributesTest extends AbstractWebTestCase
/**
* @dataProvider mapQueryStringProvider
*/
- public function testMapQueryString(array $query, string $expectedResponse, int $expectedStatusCode)
+ public function testMapQueryString(string $uri, array $query, string $expectedResponse, int $expectedStatusCode)
{
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
- $client->request('GET', '/map-query-string.json', $query);
+ $client->request('GET', $uri, $query);
$response = $client->getResponse();
+
if ($expectedResponse) {
self::assertJsonStringEqualsJsonString($expectedResponse, $response->getContent());
} else {
@@ -40,13 +42,70 @@ public function testMapQueryString(array $query, string $expectedResponse, int $
public static function mapQueryStringProvider(): iterable
{
- yield 'empty' => [
+ yield 'empty query string mapping nullable attribute' => [
+ 'uri' => '/map-query-string-to-nullable-attribute.json',
'query' => [],
'expectedResponse' => '',
'expectedStatusCode' => 204,
];
- yield 'valid' => [
+ yield 'valid query string mapping nullable attribute' => [
+ 'uri' => '/map-query-string-to-nullable-attribute.json',
+ 'query' => ['filter' => ['status' => 'approved', 'quantity' => '4']],
+ 'expectedResponse' => <<<'JSON'
+ {
+ "filter": {
+ "status": "approved",
+ "quantity": 4
+ }
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'invalid query string mapping nullable attribute' => [
+ 'uri' => '/map-query-string-to-nullable-attribute.json',
+ 'query' => ['filter' => ['status' => 'approved', 'quantity' => '200']],
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 404,
+ "detail": "filter.quantity: This value should be less than 10.",
+ "violations": [
+ {
+ "propertyPath": "filter.quantity",
+ "title": "This value should be less than 10.",
+ "template": "This value should be less than {{ compared_value }}.",
+ "parameters": {
+ "{{ value }}": "200",
+ "{{ compared_value }}": "10",
+ "{{ compared_value_type }}": "int"
+ },
+ "type": "urn:uuid:079d7420-2d13-460c-8756-de810eeb37d2"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 404,
+ ];
+
+ yield 'empty query string mapping attribute with default value' => [
+ 'uri' => '/map-query-string-to-attribute-with-default-value.json',
+ 'query' => [],
+ 'expectedResponse' => <<<'JSON'
+ {
+ "filter": {
+ "status": "approved",
+ "quantity": 5
+ }
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'valid query string mapping attribute with default value' => [
+ 'uri' => '/map-query-string-to-attribute-with-default-value.json',
'query' => ['filter' => ['status' => 'approved', 'quantity' => '4']],
'expectedResponse' => <<<'JSON'
{
@@ -59,7 +118,8 @@ public static function mapQueryStringProvider(): iterable
'expectedStatusCode' => 200,
];
- yield 'invalid' => [
+ yield 'invalid query string mapping attribute with default value' => [
+ 'uri' => '/map-query-string-to-attribute-with-default-value.json',
'query' => ['filter' => ['status' => 'approved', 'quantity' => '200']],
'expectedResponse' => <<<'JSON'
{
@@ -84,12 +144,92 @@ public static function mapQueryStringProvider(): iterable
JSON,
'expectedStatusCode' => 404,
];
+
+ $expectedResponse = <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 404,
+ "detail": "filter: This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter.",
+ "violations": [
+ {
+ "parameters": {
+ "hint": "Failed to create object because the class misses the \"filter\" property.",
+ "{{ type }}": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter"
+ },
+ "propertyPath": "filter",
+ "template": "This value should be of type {{ type }}.",
+ "title": "This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter."
+ }
+ ]
+ }
+ JSON;
+
+ $httpKernelVersion = InstalledVersions::getVersion('symfony/http-kernel');
+ if ($httpKernelVersion && version_compare($httpKernelVersion, '7.2.0', '<')) {
+ $expectedResponse = <<<'JSON'
+ {
+ "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
+ "title": "An error occurred",
+ "status": 404,
+ "detail": "Not Found"
+ }
+ JSON;
+ }
+
+ yield 'empty query string mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-query-string-to-non-nullable-attribute-without-default-value.json',
+ 'query' => [],
+ 'expectedResponse' => $expectedResponse,
+ 'expectedStatusCode' => 404,
+ ];
+
+ yield 'valid query string mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-query-string-to-non-nullable-attribute-without-default-value.json',
+ 'query' => ['filter' => ['status' => 'approved', 'quantity' => '4']],
+ 'expectedResponse' => <<<'JSON'
+ {
+ "filter": {
+ "status": "approved",
+ "quantity": 4
+ }
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'invalid query string mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-query-string-to-non-nullable-attribute-without-default-value.json',
+ 'query' => ['filter' => ['status' => 'approved', 'quantity' => '11']],
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 404,
+ "detail": "filter.quantity: This value should be less than 10.",
+ "violations": [
+ {
+ "propertyPath": "filter.quantity",
+ "title": "This value should be less than 10.",
+ "template": "This value should be less than {{ compared_value }}.",
+ "parameters": {
+ "{{ value }}": "11",
+ "{{ compared_value }}": "10",
+ "{{ compared_value_type }}": "int"
+ },
+ "type": "urn:uuid:079d7420-2d13-460c-8756-de810eeb37d2"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 404,
+ ];
}
/**
* @dataProvider mapRequestPayloadProvider
*/
- public function testMapRequestPayload(string $format, array $parameters, ?string $content, string $expectedResponse, int $expectedStatusCode)
+ public function testMapRequestPayload(string $uri, string $format, array $parameters, ?string $content, string $expectedResponse, int $expectedStatusCode)
{
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
@@ -102,7 +242,7 @@ public function testMapRequestPayload(string $format, array $parameters, ?string
$client->request(
'POST',
- '/map-request-body.'.$format,
+ $uri,
$parameters,
[],
['HTTP_ACCEPT' => $acceptHeader, 'CONTENT_TYPE' => $acceptHeader],
@@ -123,7 +263,8 @@ public function testMapRequestPayload(string $format, array $parameters, ?string
public static function mapRequestPayloadProvider(): iterable
{
- yield 'empty' => [
+ yield 'empty request mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
'parameters' => [],
'content' => '',
@@ -131,7 +272,8 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 204,
];
- yield 'valid json' => [
+ yield 'valid request with json content mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
'parameters' => [],
'content' => <<<'JSON'
@@ -149,7 +291,41 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 200,
];
- yield 'malformed json' => [
+ yield 'valid request with xml content mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.xml',
+ 'format' => 'xml',
+ 'parameters' => [],
+ 'content' => <<<'XML'
+
+ Hello everyone!
+ true
+
+ XML,
+ 'expectedResponse' => <<<'XML'
+
+ Hello everyone!
+ 1
+
+ XML,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'valid request mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
+ 'format' => 'json',
+ 'parameters' => ['comment' => 'Hello everyone!', 'approved' => '0'],
+ 'content' => null,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'malformed json request mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
'parameters' => [],
'content' => <<<'JSON'
@@ -169,7 +345,8 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 400,
];
- yield 'unsupported format' => [
+ yield 'request with unsupported format mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.dummy',
'format' => 'dummy',
'parameters' => [],
'content' => 'Hello',
@@ -177,25 +354,8 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 415,
];
- yield 'valid xml' => [
- 'format' => 'xml',
- 'parameters' => [],
- 'content' => <<<'XML'
-
- Hello everyone!
- true
-
- XML,
- 'expectedResponse' => <<<'XML'
-
- Hello everyone!
- 1
-
- XML,
- 'expectedStatusCode' => 200,
- ];
-
- yield 'invalid type' => [
+ yield 'request with invalid type mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
'parameters' => [],
'content' => <<<'JSON'
@@ -225,7 +385,8 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 422,
];
- yield 'validation error json' => [
+ yield 'invalid request with json content mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
'parameters' => [],
'content' => <<<'JSON'
@@ -267,7 +428,8 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 422,
];
- yield 'validation error xml' => [
+ yield 'invalid request with xml content mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.xml',
'format' => 'xml',
'parameters' => [],
'content' => <<<'XML'
@@ -299,22 +461,10 @@ public static function mapRequestPayloadProvider(): iterable
'expectedStatusCode' => 422,
];
- yield 'valid input' => [
- 'format' => 'json',
- 'input' => ['comment' => 'Hello everyone!', 'approved' => '0'],
- 'content' => null,
- 'expectedResponse' => <<<'JSON'
- {
- "comment": "Hello everyone!",
- "approved": false
- }
- JSON,
- 'expectedStatusCode' => 200,
- ];
-
- yield 'validation error input' => [
+ yield 'invalid request mapping nullable attribute' => [
+ 'uri' => '/map-request-to-nullable-attribute.json',
'format' => 'json',
- 'input' => ['comment' => '', 'approved' => '1'],
+ 'parameters' => ['comment' => '', 'approved' => '1'],
'content' => null,
'expectedResponse' => <<<'JSON'
{
@@ -348,32 +498,590 @@ public static function mapRequestPayloadProvider(): iterable
JSON,
'expectedStatusCode' => 422,
];
- }
-}
-class WithMapQueryStringController
-{
- public function __invoke(#[MapQueryString] ?QueryString $query): Response
- {
- if (!$query) {
- return new Response('', Response::HTTP_NO_CONTENT);
- }
+ yield 'empty request mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => '',
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
- return new JsonResponse(
- ['filter' => ['status' => $query->filter->status, 'quantity' => $query->filter->quantity]],
- );
- }
-}
+ yield 'valid request with json content mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
-class WithMapRequestPayloadController
-{
- public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $request): Response
- {
- if ('json' === $request->getPreferredFormat('json')) {
- if (!$body) {
- return new Response('', Response::HTTP_NO_CONTENT);
- }
+ yield 'valid request with xml content mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.xml',
+ 'format' => 'xml',
+ 'parameters' => [],
+ 'content' => <<<'XML'
+
+ Hello everyone!
+ true
+
+ XML,
+ 'expectedResponse' => <<<'XML'
+
+ Hello everyone!
+ 1
+
+ XML,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'valid request mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => ['comment' => 'Hello everyone!', 'approved' => '0'],
+ 'content' => null,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+ yield 'malformed json request mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false,
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
+ "title": "An error occurred",
+ "status": 400,
+ "detail": "Bad Request"
+ }
+ JSON,
+ 'expectedStatusCode' => 400,
+ ];
+
+ yield 'request with unsupported format mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.dummy',
+ 'format' => 'dummy',
+ 'parameters' => [],
+ 'content' => 'Hello',
+ 'expectedResponse' => '415 Unsupported Media Type',
+ 'expectedStatusCode' => 415,
+ ];
+
+ yield 'request with invalid type mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": "string instead of bool"
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "approved: This value should be of type bool.",
+ "violations": [
+ {
+ "propertyPath": "approved",
+ "title": "This value should be of type bool.",
+ "template": "This value should be of type {{ type }}.",
+ "parameters": {
+ "{{ type }}": "bool"
+ }
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request with json content mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "",
+ "approved": true
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "comment: This value should not be blank.\ncomment: This value is too short. It should have 10 characters or more.",
+ "violations": [
+ {
+ "propertyPath": "comment",
+ "title": "This value should not be blank.",
+ "template": "This value should not be blank.",
+ "parameters": {
+ "{{ value }}": "\"\""
+ },
+ "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
+ },
+ {
+ "propertyPath": "comment",
+ "title": "This value is too short. It should have 10 characters or more.",
+ "template": "This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
+ "parameters": {
+ "{{ value }}": "\"\"",
+ "{{ limit }}": "10",
+ "{{ value_length }}": "0"
+ },
+ "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request with xml content mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.xml',
+ 'format' => 'xml',
+ 'parameters' => [],
+ 'content' => <<<'XML'
+
+ H
+ false
+
+ XML,
+ 'expectedResponse' => <<<'XML'
+
+
+ https://symfony.com/errors/validation
+ Codestin Search App
+ 422
+ comment: This value is too short. It should have 10 characters or more.
+
+ comment
+ Codestin Search App
+ This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.
+
+ - "H"
+ - 10
+ - 1
+
+ urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45
+
+
+ XML,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request mapping attribute with default value' => [
+ 'uri' => '/map-request-to-attribute-with-default-value.json',
+ 'format' => 'json',
+ 'input' => ['comment' => '', 'approved' => '1'],
+ 'content' => null,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "comment: This value should not be blank.\ncomment: This value is too short. It should have 10 characters or more.",
+ "violations": [
+ {
+ "propertyPath": "comment",
+ "title": "This value should not be blank.",
+ "template": "This value should not be blank.",
+ "parameters": {
+ "{{ value }}": "\"\""
+ },
+ "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
+ },
+ {
+ "propertyPath": "comment",
+ "title": "This value is too short. It should have 10 characters or more.",
+ "template": "This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
+ "parameters": {
+ "{{ value }}": "\"\"",
+ "{{ limit }}": "10",
+ "{{ value_length }}": "0"
+ },
+ "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+
+ $expectedStatusCode = 400;
+ $expectedResponse = <<<'JSON'
+ {
+ "type":"https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
+ "title":"An error occurred",
+ "status":400,
+ "detail":"Bad Request"
+ }
+ JSON;
+
+ $httpKernelVersion = InstalledVersions::getVersion('symfony/http-kernel');
+ if ($httpKernelVersion && version_compare($httpKernelVersion, '7.2.0', '<')) {
+ $expectedStatusCode = 422;
+ $expectedResponse = <<<'JSON'
+ {
+ "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
+ "title": "An error occurred",
+ "status": 422,
+ "detail": "Unprocessable Content"
+ }
+ JSON;
+ }
+
+ yield 'empty request mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => '',
+ 'expectedResponse' => $expectedResponse,
+ 'expectedStatusCode' => $expectedStatusCode,
+ ];
+
+ yield 'valid request with json content mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'valid request with xml content mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.xml',
+ 'format' => 'xml',
+ 'parameters' => [],
+ 'content' => <<<'XML'
+
+ Hello everyone!
+ true
+
+ XML,
+ 'expectedResponse' => <<<'XML'
+
+ Hello everyone!
+ 1
+
+ XML,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'valid request mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => ['comment' => 'Hello everyone!', 'approved' => '0'],
+ 'content' => null,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false
+ }
+ JSON,
+ 'expectedStatusCode' => 200,
+ ];
+
+ yield 'malformed json request mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": false,
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
+ "title": "An error occurred",
+ "status": 400,
+ "detail": "Bad Request"
+ }
+ JSON,
+ 'expectedStatusCode' => 400,
+ ];
+
+ yield 'request with unsupported format mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.dummy',
+ 'format' => 'dummy',
+ 'parameters' => [],
+ 'content' => 'Hello',
+ 'expectedResponse' => '415 Unsupported Media Type',
+ 'expectedStatusCode' => 415,
+ ];
+
+ yield 'request with invalid type mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "Hello everyone!",
+ "approved": "string instead of bool"
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "approved: This value should be of type bool.",
+ "violations": [
+ {
+ "propertyPath": "approved",
+ "title": "This value should be of type bool.",
+ "template": "This value should be of type {{ type }}.",
+ "parameters": {
+ "{{ type }}": "bool"
+ }
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request with json content mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'parameters' => [],
+ 'content' => <<<'JSON'
+ {
+ "comment": "",
+ "approved": true
+ }
+ JSON,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "comment: This value should not be blank.\ncomment: This value is too short. It should have 10 characters or more.",
+ "violations": [
+ {
+ "propertyPath": "comment",
+ "title": "This value should not be blank.",
+ "template": "This value should not be blank.",
+ "parameters": {
+ "{{ value }}": "\"\""
+ },
+ "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
+ },
+ {
+ "propertyPath": "comment",
+ "title": "This value is too short. It should have 10 characters or more.",
+ "template": "This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
+ "parameters": {
+ "{{ value }}": "\"\"",
+ "{{ limit }}": "10",
+ "{{ value_length }}": "0"
+ },
+ "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request with xml content mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.xml',
+ 'format' => 'xml',
+ 'parameters' => [],
+ 'content' => <<<'XML'
+
+ H
+ false
+
+ XML,
+ 'expectedResponse' => <<<'XML'
+
+
+ https://symfony.com/errors/validation
+ Codestin Search App
+ 422
+ comment: This value is too short. It should have 10 characters or more.
+
+ comment
+ Codestin Search App
+ This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.
+
+ - "H"
+ - 10
+ - 1
+
+ urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45
+
+
+ XML,
+ 'expectedStatusCode' => 422,
+ ];
+
+ yield 'invalid request mapping non-nullable attribute without default value' => [
+ 'uri' => '/map-request-to-non-nullable-attribute-without-default-value.json',
+ 'format' => 'json',
+ 'input' => ['comment' => '', 'approved' => '1'],
+ 'content' => null,
+ 'expectedResponse' => <<<'JSON'
+ {
+ "type": "https:\/\/symfony.com\/errors\/validation",
+ "title": "Validation Failed",
+ "status": 422,
+ "detail": "comment: This value should not be blank.\ncomment: This value is too short. It should have 10 characters or more.",
+ "violations": [
+ {
+ "propertyPath": "comment",
+ "title": "This value should not be blank.",
+ "template": "This value should not be blank.",
+ "parameters": {
+ "{{ value }}": "\"\""
+ },
+ "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
+ },
+ {
+ "propertyPath": "comment",
+ "title": "This value is too short. It should have 10 characters or more.",
+ "template": "This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
+ "parameters": {
+ "{{ value }}": "\"\"",
+ "{{ limit }}": "10",
+ "{{ value_length }}": "0"
+ },
+ "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45"
+ }
+ ]
+ }
+ JSON,
+ 'expectedStatusCode' => 422,
+ ];
+ }
+}
+
+class WithMapQueryStringToNullableAttributeController
+{
+ public function __invoke(#[MapQueryString] ?QueryString $query): Response
+ {
+ if (!$query) {
+ return new Response('', Response::HTTP_NO_CONTENT);
+ }
+
+ return new JsonResponse(
+ ['filter' => ['status' => $query->filter->status, 'quantity' => $query->filter->quantity]],
+ );
+ }
+}
+
+class WithMapQueryStringToAttributeWithDefaultValueController
+{
+ public function __invoke(#[MapQueryString] QueryString $query = new QueryString(new Filter('approved', 5))): Response
+ {
+ return new JsonResponse(
+ ['filter' => ['status' => $query->filter->status, 'quantity' => $query->filter->quantity]],
+ );
+ }
+}
+
+class WithMapQueryStringToNonNullableAttributeWithoutDefaultValueController
+{
+ public function __invoke(#[MapQueryString] QueryString $query): Response
+ {
+ return new JsonResponse(
+ ['filter' => ['status' => $query->filter->status, 'quantity' => $query->filter->quantity]],
+ );
+ }
+}
+
+class WithMapRequestToNullableAttributeController
+{
+ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $request): Response
+ {
+ if ('json' === $request->getPreferredFormat('json')) {
+ if (!$body) {
+ return new Response('', Response::HTTP_NO_CONTENT);
+ }
+
+ return new JsonResponse(['comment' => $body->comment, 'approved' => $body->approved]);
+ }
+
+ return new Response(
+ <<
+ {$body->comment}
+ {$body->approved}
+
+ XML
+ );
+ }
+}
+
+class WithMapRequestToAttributeWithDefaultValueController
+{
+ public function __invoke(Request $request, #[MapRequestPayload] RequestBody $body = new RequestBody('Hello everyone!', false)): Response
+ {
+ if ('json' === $request->getPreferredFormat('json')) {
+ return new JsonResponse(['comment' => $body->comment, 'approved' => $body->approved]);
+ }
+
+ return new Response(
+ <<
+ {$body->comment}
+ {$body->approved}
+
+ XML
+ );
+ }
+}
+
+class WithMapRequestToNonNullableAttributeWithoutDefaultValueController
+{
+ public function __invoke(Request $request, #[MapRequestPayload] RequestBody $body): Response
+ {
+ if ('json' === $request->getPreferredFormat('json')) {
return new JsonResponse(['comment' => $body->comment, 'approved' => $body->approved]);
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml
index 9ec40e1708c2b..a2827eb3d07b5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml
@@ -1,7 +1,23 @@
-map_query_string:
- path: /map-query-string.{_format}
- controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapQueryStringController
+map_query_string_to_nullable_attribute:
+ path: /map-query-string-to-nullable-attribute.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapQueryStringToNullableAttributeController
-map_request_body:
- path: /map-request-body.{_format}
- controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestPayloadController
+map_query_string_to_attribute_with_default_value:
+ path: /map-query-string-to-attribute-with-default-value.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapQueryStringToAttributeWithDefaultValueController
+
+map_query_string_to_non_nullable_attribute_without_default_value:
+ path: /map-query-string-to-non-nullable-attribute-without-default-value.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapQueryStringToNonNullableAttributeWithoutDefaultValueController
+
+map_request_to_nullable_attribute:
+ path: /map-request-to-nullable-attribute.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestToNullableAttributeController
+
+map_request_to_attribute_with_default_value:
+ path: /map-request-to-attribute-with-default-value.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestToAttributeWithDefaultValueController
+
+map_request_to_non_nullable_attribute_without_default_value:
+ path: /map-request-to-non-nullable-attribute-without-default-value.{_format}
+ controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestToNonNullableAttributeWithoutDefaultValueController
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index 7ccff6f7584b1..94d04bfe4ec28 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -185,7 +185,7 @@ public static function getSubscribedEvents(): array
private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object
{
- if (!$data = $request->query->all()) {
+ if (!($data = $request->query->all()) && ($argument->isNullable() || $argument->hasDefaultValue())) {
return null;
}
@@ -212,7 +212,7 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
}
- if ('' === $data = $request->getContent()) {
+ if ('' === ($data = $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) {
return null;
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
index 7b830b041bd34..8b26767f9ea94 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
@@ -116,7 +116,7 @@ public function testNullableValueArgument()
$validator->expects($this->never())
->method('validate');
- $resolver = new RequestPayloadValueResolver(new Serializer(), $validator);
+ $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator);
$argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [
MapRequestPayload::class => new MapRequestPayload(),
@@ -138,9 +138,9 @@ public function testQueryNullableValueArgument()
$validator->expects($this->never())
->method('validate');
- $resolver = new RequestPayloadValueResolver(new Serializer(), $validator);
+ $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator);
- $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [
+ $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, true, [
MapQueryString::class => new MapQueryString(),
]);
$request = Request::create('/', 'GET');
@@ -160,7 +160,7 @@ public function testNullPayloadAndNotDefaultOrNullableArgument()
$validator->expects($this->never())
->method('validate');
- $resolver = new RequestPayloadValueResolver(new Serializer(), $validator);
+ $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator);
$argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [
MapRequestPayload::class => new MapRequestPayload(),
@@ -175,7 +175,7 @@ public function testNullPayloadAndNotDefaultOrNullableArgument()
$resolver->onKernelControllerArguments($event);
$this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class));
} catch (HttpException $e) {
- $this->assertSame(422, $e->getStatusCode());
+ $this->assertSame(400, $e->getStatusCode());
}
}
@@ -185,9 +185,9 @@ public function testQueryNullPayloadAndNotDefaultOrNullableArgument()
$validator->expects($this->never())
->method('validate');
- $resolver = new RequestPayloadValueResolver(new Serializer(), $validator);
+ $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()]), $validator);
- $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [
+ $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, false, [
MapQueryString::class => new MapQueryString(),
]);
$request = Request::create('/', 'GET');
@@ -230,7 +230,7 @@ public function testWithoutValidatorAndCouldNotDenormalize()
public function testValidationNotPassed()
{
- $content = '{"price": 50, "title": ["not a string"]}';
+ $content = '{"price": 50.0, "title": ["not a string"]}';
$serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
$validator = $this->createMock(ValidatorInterface::class);