Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ea50c48

Browse filesBrowse files
author
Rene
committed
[HttpKernel] Introduces MapUploadedFile attribute
1 parent 62144e8 commit ea50c48
Copy full SHA for ea50c48

File tree

6 files changed

+204
-2
lines changed
Filter options

6 files changed

+204
-2
lines changed

‎src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php
+137Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
1313

14+
use Symfony\Component\HttpFoundation\File\UploadedFile;
1415
use Symfony\Component\HttpFoundation\JsonResponse;
1516
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpFoundation\Response;
1718
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
1819
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
20+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
1921
use Symfony\Component\Validator\Constraints as Assert;
22+
use Symfony\Component\Validator\Constraints\File;
2023

2124
class ApiAttributesTest extends AbstractWebTestCase
2225
{
@@ -346,6 +349,117 @@ public static function mapRequestPayloadProvider(): iterable
346349
'expectedStatusCode' => 422,
347350
];
348351
}
352+
353+
public function testMapUploadedFileDefaults()
354+
{
355+
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
356+
357+
$client->request(
358+
'POST',
359+
'/map-uploaded-file-defaults',
360+
[],
361+
[
362+
'file' => new UploadedFile(__DIR__.'/Fixtures/file-small.txt', 'file-small.txt', 'text/plain'),
363+
'something-else' => new UploadedFile(__DIR__.'/Fixtures/file-big.txt', 'file-big.txt', 'text/plain'),
364+
],
365+
['HTTP_CONTENT_TYPE' => 'multipart/form-data'],
366+
);
367+
$response = $client->getResponse();
368+
369+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt', $response->getContent());
370+
}
371+
372+
public function testMapUploadedFileCustomName()
373+
{
374+
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
375+
376+
$client->request(
377+
'POST',
378+
'/map-uploaded-file-custom-name',
379+
[],
380+
[
381+
'foo' => new UploadedFile(__DIR__.'/Fixtures/file-small.txt', 'file-small.txt', 'text/plain'),
382+
'something-else' => new UploadedFile(__DIR__.'/Fixtures/file-big.txt', 'file-big.txt', 'text/plain'),
383+
],
384+
['HTTP_CONTENT_TYPE' => 'multipart/form-data'],
385+
);
386+
$response = $client->getResponse();
387+
388+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt', $response->getContent());
389+
}
390+
391+
public function testMapUploadedFileNullable()
392+
{
393+
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
394+
$client->request(
395+
'POST',
396+
'/map-uploaded-file-nullable',
397+
[],
398+
[],
399+
['HTTP_CONTENT_TYPE' => 'multipart/form-data'],
400+
);
401+
$response = $client->getResponse();
402+
403+
self::assertTrue($response->isSuccessful());
404+
self::assertEmpty($response->getContent());
405+
}
406+
407+
public function testMapUploadedFileWithConstraints()
408+
{
409+
$client = self::createClient(['test_case' => 'ApiAttributesTest']);
410+
411+
$client->request(
412+
'POST',
413+
'/map-uploaded-file-with-constraints',
414+
[],
415+
['file' => new UploadedFile(__DIR__.'/Fixtures/file-small.txt', 'file-small.txt', 'text/plain')],
416+
['HTTP_CONTENT_TYPE' => 'multipart/form-data'],
417+
);
418+
$response = $client->getResponse();
419+
420+
self::assertTrue($response->isSuccessful());
421+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt', $response->getContent());
422+
423+
$filePath = __DIR__.'/Fixtures/file-big.txt';
424+
$client->request(
425+
'POST',
426+
'/map-uploaded-file-with-constraints',
427+
[],
428+
['file' => new UploadedFile($filePath, 'file-big.txt', 'text/plain')],
429+
[
430+
'HTTP_ACCEPT' => 'application/json',
431+
'HTTP_CONTENT_TYPE' => 'multipart/form-data',
432+
],
433+
);
434+
$response = $client->getResponse();
435+
436+
$content = <<<JSON
437+
{
438+
"type": "https://symfony.com/errors/validation",
439+
"title": "Validation Failed",
440+
"status": 400,
441+
"detail": "The file is too large (71 bytes). Allowed maximum size is 50 bytes.",
442+
"violations": [
443+
{
444+
"propertyPath": "",
445+
"title": "The file is too large (71 bytes). Allowed maximum size is 50 bytes.",
446+
"template": "The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.",
447+
"parameters": {
448+
"{{ file }}": "\"$filePath\"",
449+
"{{ size }}": "71",
450+
"{{ limit }}": "50",
451+
"{{ suffix }}": "bytes",
452+
"{{ name }}": "\"file-big.txt\""
453+
},
454+
"type": "urn:uuid:df8637af-d466-48c6-a59d-e7126250a654"
455+
}
456+
]
457+
}
458+
JSON;
459+
460+
self::assertSame(400, $response->getStatusCode());
461+
self::assertJsonStringEqualsJsonString($content, $response->getContent());
462+
}
349463
}
350464

351465
class WithMapQueryStringController
@@ -385,6 +499,29 @@ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $reque
385499
}
386500
}
387501

502+
class WithMapUploadedFileController
503+
{
504+
public function defaults(#[MapUploadedFile] UploadedFile $file): Response
505+
{
506+
return new Response($file->getContent());
507+
}
508+
509+
public function customName(#[MapUploadedFile(name: 'foo')] UploadedFile $bar): Response
510+
{
511+
return new Response($bar->getContent());
512+
}
513+
514+
public function nullable(#[MapUploadedFile] ?UploadedFile $file): Response
515+
{
516+
return new Response($file?->getContent());
517+
}
518+
519+
public function withConstraints(#[MapUploadedFile(constraints: new File(maxSize: 50))] ?UploadedFile $file): Response
520+
{
521+
return new Response($file->getContent());
522+
}
523+
}
524+
388525
class QueryString
389526
{
390527
public function __construct(
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I'm not big, but I'm big enough to carry more than 50 bytes inside me.
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I'm a file with less than 50 bytes.

‎src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml
+16Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,19 @@ map_query_string:
55
map_request_body:
66
path: /map-request-body.{_format}
77
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestPayloadController
8+
9+
map_uploaded_file_defaults:
10+
path: /map-uploaded-file-defaults
11+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::defaults
12+
13+
map_uploaded_file_custom_name:
14+
path: /map-uploaded-file-custom-name
15+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::customName
16+
17+
map_uploaded_file_nullable:
18+
path: /map-uploaded-file-nullable
19+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::nullable
20+
21+
map_uploaded_file_constraints:
22+
path: /map-uploaded-file-with-constraints
23+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::withConstraints
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
15+
use Symfony\Component\Validator\Constraint;
16+
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapUploadedFile extends ValueResolver
19+
{
20+
public function __construct(
21+
public string|null $name = null,
22+
/** @var Constraint|array<Constraint>|null */
23+
public Constraint|array|null $constraints = null,
24+
string $resolver = RequestPayloadValueResolver::class,
25+
) {
26+
parent::__construct($resolver);
27+
}
28+
}

‎src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+21-2Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;
1313

14+
use Symfony\Component\HttpFoundation\File\UploadedFile;
1415
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
1718
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
19+
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
1820
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
1921
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
2022
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -56,6 +58,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
5658
$payloadMappers = [
5759
MapQueryString::class => ['mapQueryString', Response::HTTP_NOT_FOUND],
5860
MapRequestPayload::class => ['mapRequestPayload', Response::HTTP_UNPROCESSABLE_ENTITY],
61+
MapUploadedFile::class => ['mapUploadedFile', Response::HTTP_BAD_REQUEST],
5962
];
6063

6164
foreach ($payloadMappers as $mappingAttribute => [$payloadMapper, $validationFailedCode]) {
@@ -68,12 +71,13 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
6871
}
6972

7073
try {
71-
$payload = $this->$payloadMapper($request, $type, $attributes[0]);
74+
$payload = $this->$payloadMapper($request, $type, $attributes[0], $argument);
7275
} catch (PartialDenormalizationException $e) {
7376
throw new HttpException($validationFailedCode, implode("\n", array_map(static fn (NotNormalizableValueException $e) => $e->getMessage(), $e->getErrors())), $e);
7477
}
7578

76-
if (null !== $payload && \count($violations = $this->validator?->validate($payload) ?? [])) {
79+
$constraints = $attributes[0]->constraints ?? null;
80+
if (null !== $payload && \count($violations = $this->validator?->validate($payload, $constraints) ?? [])) {
7781
throw new HttpException($validationFailedCode, implode("\n", array_map(static fn (ConstraintViolationInterface $e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations));
7882
}
7983

@@ -114,4 +118,19 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
114118
throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains not valid "%s".', $format), $e);
115119
}
116120
}
121+
122+
private function mapUploadedFile(Request $request, string $type, MapUploadedFile $attribute, ArgumentMetadata $argument): ?UploadedFile
123+
{
124+
if (UploadedFile::class !== $type) {
125+
throw new \InvalidArgumentException(sprintf('Unexpected type "%s". Expected "%s".', $type, UploadedFile::class));
126+
}
127+
128+
$name = $attribute->name ?? $argument->getName();
129+
$file = $request->files->get($name);
130+
if (!$file instanceof UploadedFile) {
131+
return null;
132+
}
133+
134+
return $file;
135+
}
117136
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.