Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ea50c48

Browse files
author
Rene
committed
[HttpKernel] Introduces MapUploadedFile attribute
1 parent 62144e8 commit ea50c48

6 files changed

Lines changed: 204 additions & 2 deletions

File tree

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

Lines 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(
Lines 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.
Lines 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

Lines 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
Lines 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 <[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+
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

Lines 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)