-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[FrameworkBundle][Serializer] Add an ArgumentResolver to deserialize & validate user input #45628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
da5e8a4
3012614
6000089
4bb2235
642c7d0
2368c7f
5905ae0
406f1eb
7f7
8000
500c
eae458f
1523617
5976a4b
ebeee98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Serializer\Annotation; | ||
|
||
/** | ||
* Indicates that this argument should be deserialized and (optionally) validated. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
#[\Attribute(\Attribute::TARGET_PARAMETER)] | ||
class Input | ||
{ | ||
public function __construct(private ?string $format = null, private array $serializationContext = [], private array $validationGroups = ['Default']) | ||
{ | ||
} | ||
|
||
public function getFormat(): ?string | ||
{ | ||
return $this->format; | ||
} | ||
|
||
public function getSerializationContext(): array | ||
{ | ||
return $this->serializationContext; | ||
} | ||
|
||
public function getValidationGroups(): array | ||
{ | ||
return $this->validationGroups; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Serializer\ArgumentResolver; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
use Symfony\Component\Serializer\Annotation\Input; | ||
use Symfony\Component\Serializer\Exception\PartialDenormalizationException; | ||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
use Symfony\Component\Serializer\SerializerInterface; | ||
use Symfony\Component\Validator\ConstraintViolation; | ||
use Symfony\Component\Validator\ConstraintViolationList; | ||
use Symfony\Component\Validator\Exception\ValidationFailedException; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
|
||
/** | ||
* Deserialize & validate user input. | ||
* | ||
* Works in duo with Symfony\Bundle\FrameworkBundle\EventListener\InputValidationFailedExceptionListener. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
class UserInputResolver implements ArgumentValueResolverInterface | ||
{ | ||
public function __construct(private ValidatorInterface $validator, private SerializerInterface $serializer, ) | ||
{ | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument): bool | ||
{ | ||
return null !== $this->getAttribute($argument); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument): iterable | ||
{ | ||
$attribute = $this->getAttribute($argument); | ||
$context = array_merge($attribute->getSerializationContext(), [ | ||
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about adding |
||
]); | ||
$format = $attribute->getFormat() ?? $request->attributes->get('_format', 'json'); | ||
|
||
$input A935 = null; | ||
try { | ||
$input = $this->serializer->deserialize(data: $request->getContent(), type: $argument->getType(), format: $format, context: $context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use ordered arguments. |
||
|
||
$errors = $this->validator->validate(value: $input, groups: $attribute->getValidationGroups()); | ||
} catch (PartialDenormalizationException $e) { | ||
$errors = new ConstraintViolationList(); | ||
|
||
foreach ($e->getErrors() as $exception) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's the argument resolver's responsibility to normalize these errors, but the Serializer normalizers instead. In order not to depend on the Validator component (as I've pointed out in another comment) you can add a new |
||
$message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType()); | ||
$parameters = []; | ||
|
||
if ($exception->canUseMessageForUser()) { | ||
$parameters['hint'] = $exception->getMessage(); | ||
} | ||
|
||
$errors->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null)); | ||
} | ||
} | ||
|
||
if ($errors->count() > 0) { | ||
throw new ValidationFailedException($input, $errors); | ||
} | ||
|
||
yield $input; | ||
} | ||
|
||
private function getAttribute(ArgumentMetadata $argument): ?Input | ||
{ | ||
return $argument->getAttributes(Input::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||
<?php | ||||||
|
||||||
namespace Symfony\Component\Serializer\EventListener; | ||||||
|
||||||
use Psr\Log\LoggerInterface; | ||||||
use Symfony\Component\HttpFoundation\Response; | ||||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent; | ||||||
use Symfony\Component\Serializer\SerializerInterface; | ||||||
use Symfony\Component\Validator\Exception\ValidationFailedException; | ||||||
|
||||||
/** | ||||||
* Works in duo with Symfony\Bundle\FrameworkBundle\ArgumentResolver\UserInputResolver. | ||||||
* | ||||||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||||||
*/ | ||||||
class InputValidationFailedExceptionListener | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this listener is really necessary to return the proper response (content + status code). Instead, you can go through the ErrorRenderer system by directly adding a custom Serializer normalizer. The Framework already knows how to handle exceptions according to the request format (see SearializerErrorRenderer). |
||||||
{ | ||||||
public function __construct(private SerializerInterface $serializer, private LoggerInterface $logger) | ||||||
{ | ||||||
} | ||||||
|
||||||
public function __invoke(ExceptionEvent $event): void | ||||||
{ | ||||||
$throwable = $event->getThrowable(); | ||||||
$format = $event->getRequest()->attributes->get('_format', 'json'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better use
Suggested change
|
||||||
|
||||||
if (!$throwable instanceof ValidationFailedException) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is potentially dangerous. Existing application might already use this exception and your listener will add unexpected sematics to it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed! Validation is now optional as well |
||||||
return; | ||||||
} | ||||||
|
||||||
$response = new Response($this->serializer->serialize($throwable->getViolations(), $format), Response::HTTP_UNPROCESSABLE_ENTITY); | ||||||
$this->logger->info('Invalid input rejected: "{reason}"', ['reason' => (string) $throwable->getViolations()]); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not super sure what to log here, as violations might contain sensitive information? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO, the reasons are not really useful in the log |
||||||
|
||||||
$event->setResponse($response); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about stopping propagation as this listener is only here to create the HTTP response? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
} | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.