Skip to content

Commit b3a06fb

Browse files
Renerenedelima
authored andcommitted
[HttpKernel] Add MapUploadedFile attribute
1 parent b5ee977 commit b3a06fb

File tree

6 files changed

+405
-10
lines changed

6 files changed

+405
-10
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|null $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
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `HttpException::fromStatusCode()`
99
* Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails
1010
* Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved
11+
* Add `#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments
1112

1213
7.0
1314
---

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

Lines changed: 26 additions & 10 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;
@@ -28,6 +30,7 @@
2830
use Symfony\Component\Serializer\Exception\UnsupportedFormatException;
2931
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3032
use Symfony\Component\Serializer\SerializerInterface;
33+
use Symfony\Component\Validator\Constraints as Assert;
3134
use Symfony\Component\Validator\ConstraintViolation;
3235
use Symfony\Component\Validator\ConstraintViolationList;
3336
use Symfony\Component\Validator\Exception\ValidationFailedException;
@@ -68,13 +71,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
6871
{
6972
$attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0]
7073
?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0]
74+
?? $argument->getAttributesOfType(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0]
7175
?? null;
7276

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

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

@@ -94,19 +98,22 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
9498
} elseif ($argument instanceof MapRequestPayload) {
9599
$payloadMapper = 'mapRequestPayload';
96100
$validationFailedCode = $argument->validationFailedStatusCode;
101+
} elseif ($argument instanceof MapUploadedFile) {
102+
$payloadMapper = 'mapUploadedFile';
103+
$validationFailedCode = $argument->validationFailedStatusCode;
97104
} else {
98105
continue;
99106
}
100107
$request = $event->getRequest();
101108

102-
if (!$type = $argument->metadata->getType()) {
109+
if (!$argument->metadata->getType()) {
103110
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName()));
104111
}
105112

106113
if ($this->validator) {
107114
$violations = new ConstraintViolationList();
108115
try {
109-
$payload = $this->$payloadMapper($request, $type, $argument);
116+
$payload = $this->$payloadMapper($request, $argument->metadata, $argument);
110117
} catch (PartialDenormalizationException $e) {
111118
$trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p);
112119
foreach ($e->getErrors() as $error) {
@@ -126,15 +133,19 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
126133
}
127134

128135
if (null !== $payload && !\count($violations)) {
129-
$violations->addAll($this->validator->validate($payload, null, $argument->validationGroups ?? null));
136+
$constraints = $argument->constraints ?? null;
137+
if (\is_array($payload) && !empty($constraints) && !$constraints instanceof Assert\All) {
138+
$constraints = new Assert\All($constraints);
139+
}
140+
$violations->addAll($this->validator->validate($payload, $constraints, $argument->validationGroups ?? null));
130141
}
131142

132143
if (\count($violations)) {
133144
throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations));
134145
}
135146
} else {
136147
try {
137-
$payload = $this->$payloadMapper($request, $type, $argument);
148+
$payload = $this->$payloadMapper($request, $argument->metadata, $argument);
138149
} catch (PartialDenormalizationException $e) {
139150
throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e);
140151
}
@@ -161,16 +172,16 @@ public static function getSubscribedEvents(): array
161172
];
162173
}
163174

164-
private function mapQueryString(Request $request, string $type, MapQueryString $attribute): ?object
175+
private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object
165176
{
166177
if (!$data = $request->query->all()) {
167178
return null;
168179
}
169180

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

173-
private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object
184+
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): ?object
174185
{
175186
if (null === $format = $request->getContentTypeFormat()) {
176187
throw new UnsupportedMediaTypeHttpException('Unsupported format.');
@@ -181,7 +192,7 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
181192
}
182193

183194
if ($data = $request->request->all()) {
184-
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
195+
return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
185196
}
186197

187198
if ('' === $data = $request->getContent()) {
@@ -193,7 +204,7 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
193204
}
194205

195206
try {
196-
return $this->serializer->deserialize($data, $type, $format, self::CONTEXT_DESERIALIZE + $attribute->serializationContext);
207+
return $this->serializer->deserialize($data, $argument->getType(), $format, self::CONTEXT_DESERIALIZE + $attribute->serializationContext);
197208
} catch (UnsupportedFormatException $e) {
198209
throw new UnsupportedMediaTypeHttpException(sprintf('Unsupported format: "%s".', $format), $e);
199210
} catch (NotEncodableValueException $e) {
@@ -202,4 +213,9 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay
202213
throw new BadRequestHttpException(sprintf('Request payload contains invalid "%s" property.', $e->property), $e);
203214
}
204215
}
216+
217+
private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null
218+
{
219+
return $request->files->get($attribute->name ?? $argument->getName(), []);
220+
}
205221
}

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