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 13ab9eb

Browse filesBrowse files
committed
feature #49978 [HttpKernel] Introduce #[MapUploadedFile] controller argument attribute (renedelima)
This PR was merged into the 7.1 branch. Discussion ---------- [HttpKernel] Introduce `#[MapUploadedFile]` controller argument attribute | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #52678, #49138 | License | MIT | Doc PR | - ## Usage Example ```php # src/Controller/UserPictureController.php namespace App\Controller; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Constraints as Assert; #[AsController] final class UserPictureController { #[Route('/user/picture', methods: ['PUT'])] public function __invoke( #[MapUploadedFile( new Assert\File(mimeTypes: ['image/png', 'image/jpeg']), )] ?UploadedFile $picture, ): Response { return new Response('Your picture was updated'); } } ``` ```php # src/Controller/UserDocumentsController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Constraints as Assert; final class UserDocumentsController { #[Route('/user/documents', methods: ['PUT'])] public function __invoke( #[MapUploadedFile( new Assert\File(mimeTypes: ['application/pdf']) )] array $documents ): Response { return new Response('Thanks for sharing your documents'); } } ``` ```php # src/Controller/UserDocumentsController.php namespace App\Controller; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Constraints as Assert; final class UserDocumentsController { #[Route('/user/documents', methods: ['PUT'])] public function __invoke( #[MapUploadedFile( new Assert\File(mimeTypes: ['application/pdf']) )] UploadedFile ...$documents ): Response { return new Response('Thanks for sharing your documents'); } } ``` Commits ------- b85dbd0 [HttpKernel] Add MapUploadedFile attribute
2 parents f02b0f2 + b85dbd0 commit 13ab9eb
Copy full SHA for 13ab9eb

File tree

6 files changed

+408
-11
lines changed
Filter options

6 files changed

+408
-11
lines changed
+33Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
use Symfony\Component\Validator\Constraint;
18+
19+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
20+
class MapUploadedFile extends ValueResolver
21+
{
22+
public ArgumentMetadata $metadata;
23+
24+
public function __construct(
25+
/** @var Constraint|array<Constraint>|null */
26+
public Constraint|array|null $constraints = null,
27+
public ?string $name = null,
28+
string $resolver = RequestPayloadValueResolver::class,
29+
public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY,
30+
) {
31+
parent::__construct($resolver);
32+
}
33+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved
1111
* Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items
1212
* Deprecate `Extension::addAnnotatedClassesToCompile()` and related code infrastructure
13+
* Add `#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments
1314

1415
7.0
1516
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+29-11Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;
1313

1414
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\File\UploadedFile;
1516
use Symfony\Component\HttpFoundation\Request;
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\Event\ControllerArgumentsEvent;
@@ -29,6 +31,7 @@
2931
use Symfony\Component\Serializer\Exception\UnsupportedFormatException;
3032
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3133
use Symfony\Component\Serializer\SerializerInterface;
34+
use Symfony\Component\Validator\Constraints as Assert;
3235
use Symfony\Component\Validator\ConstraintViolation;
3336
use Symfony\Component\Validator\ConstraintViolationList;
3437
use Symfony\Component\Validator\Exception\ValidationFailedException;
@@ -69,13 +72,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
6972
{
7073
$attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0]
7174
?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0]
75+
?? $argument->getAttributesOfType(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0]
7276
?? null;
7377

7478
if (!$attribute) {
7579
return [];
7680
}
7781

78-
if ($argument->isVariadic()) {
82+
if (!$attribute instanceof MapUploadedFile && $argument->isVariadic()) {
7983
throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
8084
}
8185

@@ -100,24 +104,27 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
100104

101105
foreach ($arguments as $i => $argument) {
102106
if ($argument instanceof MapQueryString) {
103-
$payloadMapper = 'mapQueryString';
107+
$payloadMapper = $this->mapQueryString(...);
104108
$validationFailedCode = $argument->validationFailedStatusCode;
105109
} elseif ($argument instanceof MapRequestPayload) {
106-
$payloadMapper = 'mapRequestPayload';
110+
$payloadMapper = $this->mapRequestPayload(...);
111+
$validationFailedCode = $argument->validationFailedStatusCode;
112+
} elseif ($argument instanceof MapUploadedFile) {
113+
$payloadMapper = $this->mapUploadedFile(...);
107114
$validationFailedCode = $argument->validationFailedStatusCode;
108115
} else {
109116
continue;
110117
}
111118
$request = $event->getRequest();
112119

113-
if (!$type = $argument->metadata->getType()) {
120+
if (!$argument->metadata->getType()) {
114121
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName()));
115122
}
116123

117124
if ($this->validator) {
118125
$violations = new ConstraintViolationList();
119126
try {
120-
$payload = $this->$payloadMapper($request, $type, $argument);
127+
$payload = $payloadMapper($request, $argument->metadata, $argument);
121128
} catch (PartialDenormalizationException $e) {
122129
$trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p);
123130
foreach ($e->getErrors() as $error) {
@@ -137,15 +144,19 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
137144
}
138145

139146
if (null !== $payload && !\count($violations)) {
140-
$violations->addAll($this->validator->validate($payload, null, $argument->validationGroups ?? null));
147+
$constraints = $argument->constraints ?? null;
148+
if (\is_array($payload) && !empty($constraints) && !$constraints instanceof Assert\All) {
149+
$constraints = new Assert\All($constraints);
150+
}
151+
$violations->addAll($this->validator->validate($payload, $constraints, $argument->validationGroups ?? null));
141152
}
142153

143154
if (\count($violations)) {
144155
throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations));
145156
}
146157
} else {
147158
try {
148-
$payload = $this->$payloadMapper($request, $type, $argument);
159+
$payload = $payloadMapper($request, $argument->metadata, $argument);
149160
} catch (PartialDenormalizationException $e) {
150161
throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e);
151162
}
@@ -172,16 +183,16 @@ public static function getSubscribedEvents(): array
172183
];
173184
}
174185

175-
private function mapQueryString(Request $request, string $type, MapQueryString $attribute): ?object
186+
private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object
176187
{
177188
if (!$data = $request->query->all()) {
178189
return null;
179190
}
180191

181-
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
192+
return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
182193
}
183194

184-
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): object|array|null
195+
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null
185196
{
186197
if (null === $format = $request->getContentTypeFormat()) {
187198
throw new UnsupportedMediaTypeHttpException('Unsupported format.');
@@ -191,8 +202,10 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
191202
throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format));
192203
}
193204

194-
if ('array' === $type && null !== $attribute->type) {
205+
if ('array' === $argument->getType() && null !== $attribute->type) {
195206
$type = $attribute->type.'[]';
207+
} else {
208+
$type = $argument->getType();
196209
}
197210

198211
if ($data = $request->request->all()) {
@@ -217,4 +230,9 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
217230
throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" property.', $e->property), $e);
218231
}
219232
}
233+
234+
private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null
235+
{
236+
return $request->files->get($attribute->name ?? $argument->getName(), []);
237+
}
220238
}

0 commit comments

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