Skip to content

Commit e96d7ee

Browse files
committed
[HttpKernel] Add MapUploadedFile attribute
Signed-off-by: Rene Lima <renedelima@gmail.com>
1 parent 9b323c6 commit e96d7ee

File tree

6 files changed

+408
-11
lines changed

6 files changed

+408
-11
lines changed
Lines 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

Lines 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

Lines 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy