Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

Commitb6fa35a

Browse files
author
Rene
committed
[HttpKernel] Add MapUploadedFile attribute
1 parentbdd90ea commitb6fa35a

File tree

7 files changed

+274
-9
lines changed

7 files changed

+274
-9
lines changed

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

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespaceSymfony\Bundle\FrameworkBundle\Tests\Functional;
1313

14+
useSymfony\Component\HttpFoundation\File\UploadedFile;
1415
useSymfony\Component\HttpFoundation\JsonResponse;
1516
useSymfony\Component\HttpFoundation\Request;
1617
useSymfony\Component\HttpFoundation\Response;
1718
useSymfony\Component\HttpKernel\Attribute\MapQueryString;
1819
useSymfony\Component\HttpKernel\Attribute\MapRequestPayload;
20+
useSymfony\Component\HttpKernel\Attribute\MapUploadedFile;
1921
useSymfony\Component\Validator\ConstraintsasAssert;
2022

2123
class ApiAttributesTestextends AbstractWebTestCase
@@ -346,6 +348,172 @@ public static function mapRequestPayloadProvider(): iterable
346348
'expectedStatusCode' =>422,
347349
];
348350
}
351+
352+
publicfunctiontestMapUploadedFileDefaults()
353+
{
354+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
355+
356+
$client->request(
357+
'POST',
358+
'/map-uploaded-file-defaults',
359+
[],
360+
[
361+
'file' =>newUploadedFile(__DIR__.'/Fixtures/file-small.txt','file-small.txt','text/plain'),
362+
'something-else' =>newUploadedFile(__DIR__.'/Fixtures/file-big.txt','file-big.txt','text/plain'),
363+
],
364+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
365+
);
366+
$response =$client->getResponse();
367+
368+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt',$response->getContent());
369+
}
370+
371+
publicfunctiontestMapUploadedFileCustomName()
372+
{
373+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
374+
375+
$client->request(
376+
'POST',
377+
'/map-uploaded-file-custom-name',
378+
[],
379+
[
380+
'foo' =>newUploadedFile(__DIR__.'/Fixtures/file-small.txt','file-small.txt','text/plain'),
381+
'something-else' =>newUploadedFile(__DIR__.'/Fixtures/file-big.txt','file-big.txt','text/plain'),
382+
],
383+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
384+
);
385+
$response =$client->getResponse();
386+
387+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt',$response->getContent());
388+
}
389+
390+
publicfunctiontestMapUploadedFileNullable()
391+
{
392+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
393+
$client->request(
394+
'POST',
395+
'/map-uploaded-file-nullable',
396+
[],
397+
[],
398+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
399+
);
400+
$response =$client->getResponse();
401+
402+
self::assertTrue($response->isSuccessful());
403+
self::assertEmpty($response->getContent());
404+
}
405+
406+
publicfunctiontestMapUploadedFileWithConstraints()
407+
{
408+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
409+
410+
$client->request(
411+
'POST',
412+
'/map-uploaded-file-with-constraints',
413+
[],
414+
['file' =>newUploadedFile(__DIR__.'/Fixtures/file-small.txt','file-small.txt','text/plain')],
415+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
416+
);
417+
$response =$client->getResponse();
418+
419+
self::assertTrue($response->isSuccessful());
420+
self::assertStringEqualsFile(__DIR__.'/Fixtures/file-small.txt',$response->getContent());
421+
422+
$filePath =__DIR__.'/Fixtures/file-big.txt';
423+
$client->request(
424+
'POST',
425+
'/map-uploaded-file-with-constraints',
426+
[],
427+
['file' =>newUploadedFile($filePath,'file-big.txt','text/plain')],
428+
[
429+
'HTTP_ACCEPT' =>'application/json',
430+
'HTTP_CONTENT_TYPE' =>'multipart/form-data',
431+
],
432+
);
433+
$response =$client->getResponse();
434+
435+
$content =<<<JSON
436+
{
437+
"type": "https://symfony.com/errors/validation",
438+
"title": "Validation Failed",
439+
"status": 422,
440+
"detail": "The file is too large (71 bytes). Allowed maximum size is 50 bytes.",
441+
"violations": [
442+
{
443+
"propertyPath": "",
444+
"title": "The file is too large (71 bytes). Allowed maximum size is 50 bytes.",
445+
"template": "The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.",
446+
"parameters": {
447+
"{{ file }}": "\"$filePath\"",
448+
"{{ size }}": "71",
449+
"{{ limit }}": "50",
450+
"{{ suffix }}": "bytes",
451+
"{{ name }}": "\"file-big.txt\""
452+
},
453+
"type": "urn:uuid:df8637af-d466-48c6-a59d-e7126250a654"
454+
}
455+
]
456+
}
457+
JSON;
458+
459+
self::assertSame(422,$response->getStatusCode());
460+
self::assertJsonStringEqualsJsonString($content,$response->getContent());
461+
}
462+
463+
publicfunctiontestMapUploadedFileWithMultipleFilesArray()
464+
{
465+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
466+
467+
$client->request(
468+
'POST',
469+
'/map-uploaded-file-with-multiple-array',
470+
[],
471+
[
472+
'files' => [
473+
newUploadedFile(__DIR__.'/Fixtures/file-small.txt','file-small.txt','text/plain'),
474+
newUploadedFile(__DIR__.'/Fixtures/file-big.txt','file-small.txt','text/plain'),
475+
],
476+
],
477+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
478+
);
479+
$response =$client->getResponse();
480+
481+
self::assertTrue($response->isSuccessful());
482+
self::assertJsonStringEqualsJsonString(
483+
json_encode([2, UploadedFile::class, UploadedFile::class], \JSON_THROW_ON_ERROR),
484+
$response->getContent()
485+
);
486+
}
487+
488+
publicfunctiontestMapUploadedFileWithMultipleFilesVariadic()
489+
{
490+
$client =self::createClient(['test_case' =>'ApiAttributesTest']);
491+
492+
$client->request(
493+
'POST',
494+
'/map-uploaded-file-with-multiple-variadic',
495+
[],
496+
[
497+
'foo' => [
498+
newUploadedFile(__DIR__.'/Fixtures/file-small.txt','first.txt','text/plain'),
499+
newUploadedFile(__DIR__.'/Fixtures/file-small.txt','second.txt','text/plain'),
500+
newUploadedFile(__DIR__.'/Fixtures/file-small.txt','third.txt','text/plain'),
501+
],
502+
'bar' => [
503+
newUploadedFile(__DIR__.'/Fixtures/file-big.txt','big.txt','text/plain'),
504+
newUploadedFile(__DIR__.'/Fixtures/file-big.txt','huge.txt','text/plain'),
505+
],
506+
],
507+
['HTTP_CONTENT_TYPE' =>'multipart/form-data'],
508+
);
509+
$response =$client->getResponse();
510+
511+
self::assertTrue($response->isSuccessful());
512+
self::assertJsonStringEqualsJsonString(
513+
json_encode([3,'first.txt','second.txt','third.txt'], \JSON_THROW_ON_ERROR),
514+
$response->getContent()
515+
);
516+
}
349517
}
350518

351519
class WithMapQueryStringController
@@ -385,6 +553,39 @@ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $reque
385553
}
386554
}
387555

556+
class WithMapUploadedFileController
557+
{
558+
publicfunctiondefaults(#[MapUploadedFile]UploadedFile$file):Response
559+
{
560+
returnnewResponse($file->getContent());
561+
}
562+
563+
publicfunctioncustomName(#[MapUploadedFile(name:'foo')]UploadedFile$bar):Response
564+
{
565+
returnnewResponse($bar->getContent());
566+
}
567+
568+
publicfunctionnullable(#[MapUploadedFile] ?UploadedFile$file):Response
569+
{
570+
returnnewResponse($file?->getContent());
571+
}
572+
573+
publicfunctionwithConstraints(#[MapUploadedFile(constraints:newAssert\File(maxSize:50))] ?UploadedFile$file):Response
574+
{
575+
returnnewResponse($file->getContent());
576+
}
577+
578+
publicfunctionwithMultipleFilesArray(#[MapUploadedFile(constraints:newAssert\All([newAssert\File(maxSize:100)]))] ?array$files):JsonResponse
579+
{
580+
returnnewJsonResponse([\count($files),\get_class($files[0]),\get_class($files[1])]);
581+
}
582+
583+
publicfunctionwithMultipleFilesVariadic(#[MapUploadedFile(constraints:newAssert\All([newAssert\File(maxSize:100)]))]UploadedFile ...$foo):JsonResponse
584+
{
585+
returnnewJsonResponse([\count($foo), ...array_map(staticfn($current) =>$current->getClientOriginalName(),$foo)]);
586+
}
587+
}
588+
388589
class QueryString
389590
{
390591
publicfunction__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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,27 @@ 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
24+
25+
map_uploaded_file_multiple_array:
26+
path:/map-uploaded-file-with-multiple-array
27+
controller:Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::withMultipleFilesArray
28+
29+
map_uploaded_file_multiple_variadic:
30+
path:/map-uploaded-file-with-multiple-variadic
31+
controller:Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapUploadedFileController::withMultipleFilesVariadic
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 <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+
namespaceSymfony\Component\HttpKernel\Attribute;
13+
14+
useSymfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
15+
useSymfony\Component\Validator\Constraint;
16+
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapUploadedFileextends ValueResolver
19+
{
20+
publicfunction__construct(
21+
publicstring|null$name =null,
22+
/** @var Constraint|array<Constraint>|null */
23+
publicConstraint|array|null$constraints =null,
24+
string$resolver = RequestPayloadValueResolver::class,
25+
) {
26+
parent::__construct($resolver);
27+
}
28+
}

‎src/Symfony/Component/HttpKernel/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Introduce targeted value resolvers with`#[ValueResolver]` and`#[AsTargetedValueResolver]`
1414
* Add`#[MapRequestPayload]` to map and validate request payload from`Request::getContent()` or`Request::$request->all()` to typed objects
1515
* Add`#[MapQueryString]` to map and validate request query string from`Request::$query->all()` to typed objects
16+
* Add`#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments
1617

1718
6.2
1819
---

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespaceSymfony\Component\HttpKernel\Controller\ArgumentResolver;
1313

14+
useSymfony\Component\HttpFoundation\File\UploadedFile;
1415
useSymfony\Component\HttpFoundation\Request;
1516
useSymfony\Component\HttpFoundation\Response;
1617
useSymfony\Component\HttpKernel\Attribute\MapQueryString;
1718
useSymfony\Component\HttpKernel\Attribute\MapRequestPayload;
19+
useSymfony\Component\HttpKernel\Attribute\MapUploadedFile;
1820
useSymfony\Component\HttpKernel\Controller\ValueResolverInterface;
1921
useSymfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
2022
useSymfony\Component\HttpKernel\Exception\HttpException;
@@ -62,48 +64,50 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
6264
$payloadMappers = [
6365
MapQueryString::class => ['mapQueryString', Response::HTTP_NOT_FOUND],
6466
MapRequestPayload::class => ['mapRequestPayload', Response::HTTP_UNPROCESSABLE_ENTITY],
67+
MapUploadedFile::class => ['mapUploadedFile', Response::HTTP_UNPROCESSABLE_ENTITY],
6568
];
6669

6770
foreach ($payloadMappersas$mappingAttribute => [$payloadMapper,$validationFailedCode]) {
6871
if (!$attributes =$argument->getAttributesOfType($mappingAttribute, ArgumentMetadata::IS_INSTANCEOF)) {
6972
continue;
7073
}
7174

72-
if (!$type =$argument->getType()) {
75+
if (!$argument->getType()) {
7376
thrownew \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.',$argument->getName()));
7477
}
7578

7679
try {
77-
$payload =$this->$payloadMapper($request,$type,$attributes[0]);
80+
$payload =$this->$payloadMapper($request,$argument,$attributes[0]);
7881
}catch (PartialDenormalizationException$e) {
7982
thrownewHttpException($validationFailedCode,implode("\n",array_map(staticfn (NotNormalizableValueException$e) =>$e->getMessage(),$e->getErrors())),$e);
8083
}
8184

82-
if (null !==$payload &&\count($violations =$this->validator?->validate($payload) ?? [])) {
85+
$constraints =$attributes[0]->constraints ??null;
86+
if (null !==$payload &&\count($violations =$this->validator?->validate($payload,$constraints) ?? [])) {
8387
thrownewHttpException($validationFailedCode,implode("\n",array_map(staticfn (ConstraintViolationInterface$e) =>$e->getMessage(),iterator_to_array($violations))),newValidationFailedException($payload,$violations));
8488
}
8589

8690
if (null !==$payload ||$argument->isNullable()) {
87-
return [$payload];
91+
return$argument->isVariadic() &&is_array($payload) ?$payload :[$payload];
8892
}
8993
}
9094

9195
return [];
9296
}
9397

94-
privatefunctionmapQueryString(Request$request,string$type,MapQueryString$attribute): ?object
98+
privatefunctionmapQueryString(Request$request,ArgumentMetadata$argument,MapQueryString$attribute): ?object
9599
{
96100
if (!$data =$request->query->all()) {
97101
returnnull;
98102
}
99103

100-
return$this->serializer->denormalize($data,$type,self::DEFAULT_FORMAT,self::CONTEXT_DENORMALIZE +$attribute->context);
104+
return$this->serializer->denormalize($data,$argument->getType(),self::DEFAULT_FORMAT,self::CONTEXT_DENORMALIZE +$attribute->context);
101105
}
102106

103-
privatefunctionmapRequestPayload(Request$request,string$type,MapRequestPayload$attribute): ?object
107+
privatefunctionmapRequestPayload(Request$request,ArgumentMetadata$argument,MapRequestPayload$attribute): ?object
104108
{
105109
if ($data =$request->request->all()) {
106-
return$this->serializer->denormalize($data,$type,self::DEFAULT_FORMAT,self::CONTEXT_DENORMALIZE +$attribute->context);
110+
return$this->serializer->denormalize($data,$argument->getType(),self::DEFAULT_FORMAT,self::CONTEXT_DENORMALIZE +$attribute->context);
107111
}
108112

109113
if ('' ===$data =$request->getContent()) {
@@ -115,11 +119,16 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
115119
}
116120

117121
try {
118-
return$this->serializer->deserialize($data,$type,$format,self::CONTEXT_DESERIALIZE +$attribute->context);
122+
return$this->serializer->deserialize($data,$argument->getType(),$format,self::CONTEXT_DESERIALIZE +$attribute->context);
119123
}catch (UnsupportedFormatException$e) {
120124
thrownewHttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE,sprintf('Unsupported format: "%s".',$format),$e);
121125
}catch (NotEncodableValueException$e) {
122126
thrownewHttpException(Response::HTTP_BAD_REQUEST,sprintf('Request payload contains not valid "%s".',$format),$e);
123127
}
124128
}
129+
130+
privatefunctionmapUploadedFile(Request$request,ArgumentMetadata$argument,MapUploadedFile$attribute):UploadedFile|array|null
131+
{
132+
return$request->files->get($attribute->name ??$argument->getName());
133+
}
125134
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp