From ce650a9a38df56b8914779022f001671246e2a35 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Mon, 16 Jan 2023 21:56:46 +0100 Subject: [PATCH 1/4] add RequestValidator --- .../Resources/config/validator.php | 8 ++ .../Validator/Attribute/RequestValidator.php | 29 ++++++ .../RequestValidationSubscriber.php | 88 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/Symfony/Component/Validator/Attribute/RequestValidator.php create mode 100644 src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php index c397e73d42505..116417132ed80 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php @@ -19,6 +19,7 @@ use Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator; use Symfony\Component\Validator\Constraints\WhenValidator; use Symfony\Component\Validator\ContainerConstraintValidatorFactory; +use Symfony\Component\Validator\EventListener\RequestValidationSubscriber; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -109,5 +110,12 @@ service('property_info'), ]) ->tag('validator.auto_mapper') + + ->set('validator.request_validator', RequestValidationSubscriber::class) + ->args([ + service('serializer')->nullOnInvalid(), + service('validator') + ]) + ->tag('kernel.event_subscriber') ; }; diff --git a/src/Symfony/Component/Validator/Attribute/RequestValidator.php b/src/Symfony/Component/Validator/Attribute/RequestValidator.php new file mode 100644 index 0000000000000..d00fc345e62ff --- /dev/null +++ b/src/Symfony/Component/Validator/Attribute/RequestValidator.php @@ -0,0 +1,29 @@ +class = $class; + $this->finalize = $finalize; + } + + public function getClass(): string + { + return $this->class; + } + + public function getFinalize(): ?string + { + return $this->finalize; + } +} diff --git a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php new file mode 100644 index 0000000000000..ff406d7ea6697 --- /dev/null +++ b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php @@ -0,0 +1,88 @@ + 'validateRequest' + ]; + } + + public function validateRequest(ControllerArgumentsEvent $event): void { + $controller = $event->getController(); + $arguments = $event->getArguments(); + $reflectionMethod = $this->getReflectionMethod($controller); + $request = $event->getRequest(); + + $attributes = $reflectionMethod->getAttributes(RequestValidator::class, \ReflectionAttribute::IS_INSTANCEOF); + + if(count($attributes) === 0) { + return; + } + + // only first attribute can validate + $attribute = $attributes[0]; + + $class = $attribute->getArguments()['class']; + + $object = new $class(); + + // if serializer is installed serialize input body + if(null !== $this->serializer) { + $object = $this->serializer->deserialize($request->getContent(), $class, 'json'); + } + + // set input variables in object + foreach ($request->request as $key => $input) { + $object->{$key} = $input; + } + + // set parameter variables in object + foreach ($request->attributes as $key => $parameter) { + $object->{$key} = $parameter; + } + + $violations = $this->validator->validate($object); + + if(count($violations) > 0) { + throw new ValidationFailedException(sprintf("Validation of %s failed!", $class), $violations); + } + + foreach ($arguments as $index => $argument) { + if(!$argument instanceof $class) { + continue; + } + $arguments[$index] = $object; + } + + $event->setArguments($arguments); + } + + private function getReflectionMethod(callable $controller): \ReflectionMethod + { + if (is_array($controller)) { + $class = $controller[0]; + $method = $controller[1]; + } else { + /** @var object $controller */ + $class = $controller; + $method = '__invoke'; + } + + return new \ReflectionMethod($class, $method); + } +} From e88d5abf79194dcd03149820bc1e93aa3ae2a2e4 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Mon, 16 Jan 2023 22:59:03 +0100 Subject: [PATCH 2/4] add order, override flag and serializerFormat --- .../Resources/config/validator.php | 2 +- .../Validator/Attribute/RequestValidator.php | 32 +++++----- .../RequestValidationSubscriber.php | 61 ++++++++++++++----- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php index 116417132ed80..6eaafb704afdc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php @@ -113,8 +113,8 @@ ->set('validator.request_validator', RequestValidationSubscriber::class) ->args([ + service('validator'), service('serializer')->nullOnInvalid(), - service('validator') ]) ->tag('kernel.event_subscriber') ; diff --git a/src/Symfony/Component/Validator/Attribute/RequestValidator.php b/src/Symfony/Component/Validator/Attribute/RequestValidator.php index d00fc345e62ff..ef37b6b300707 100644 --- a/src/Symfony/Component/Validator/Attribute/RequestValidator.php +++ b/src/Symfony/Component/Validator/Attribute/RequestValidator.php @@ -5,25 +5,21 @@ #[\Attribute(\Attribute::TARGET_FUNCTION)] final class RequestValidator { - private string $class; - private ?string $finalize; + public const ORDER_ATTRIBUTES = 'attributes'; + public const ORDER_SERIALIZE = 'serialize'; + public const ORDER_QUERY = 'query'; + public const ORDER_REQUEST = 'request'; public function __construct( - string $class, - ?string $finalize = null - ) - { - $this->class = $class; - $this->finalize = $finalize; - } - - public function getClass(): string - { - return $this->class; - } - - public function getFinalize(): ?string - { - return $this->finalize; + public string $class, + public bool $override = true, + public array $order = [ + self::ORDER_SERIALIZE, + self::ORDER_ATTRIBUTES, + self::ORDER_QUERY, + self::ORDER_REQUEST, + ], + public string $serializedFormat = 'json' + ) { } } diff --git a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php index ff406d7ea6697..0552e20bbce85 100644 --- a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php +++ b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php @@ -4,15 +4,19 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Attribute\RequestValidator; +use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Validator\ValidatorInterface; class RequestValidationSubscriber implements EventSubscriberInterface { - public function __construct(readonly ?SerializerInterface $serializer = null, readonly ValidatorInterface $validator) - { + public function __construct( + readonly ValidatorInterface $validator, + readonly ?SerializerInterface $serializer = null + ) { } public static function getSubscribedEvents() @@ -30,7 +34,7 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $attributes = $reflectionMethod->getAttributes(RequestValidator::class, \ReflectionAttribute::IS_INSTANCEOF); - if(count($attributes) === 0) { + if (count($attributes) === 0) { return; } @@ -38,22 +42,30 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $attribute = $attributes[0]; $class = $attribute->getArguments()['class']; + $override = $attribute->getArguments()['override']; + $serializedFormat = $attribute->getArguments()['serializedFormat']; + $order = $attribute->getArguments()['order']; $object = new $class(); - // if serializer is installed serialize input body - if(null !== $this->serializer) { - $object = $this->serializer->deserialize($request->getContent(), $class, 'json'); - } - - // set input variables in object - foreach ($request->request as $key => $input) { - $object->{$key} = $input; - } + foreach ($order as $type) { + switch ($type) { + case RequestValidator::ORDER_SERIALIZE: + $serializer = $this->getSerializer(); + $serializer->deserialize($request->getContent(), $class, $serializedFormat, + [AbstractNormalizer::OBJECT_TO_POPULATE => $object]); + continue 2; + case RequestValidator::ORDER_REQUEST: + $this->setProperties($object, $request->request, $override); + break; + case RequestValidator::ORDER_QUERY: + $this->setProperties($object, $request->query, $override); + break; + case RequestValidator::ORDER_ATTRIBUTES: + $this->setProperties($object, $request->attributes, $override); + break; + } - // set parameter variables in object - foreach ($request->attributes as $key => $parameter) { - $object->{$key} = $parameter; } $violations = $this->validator->validate($object); @@ -72,6 +84,15 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $event->setArguments($arguments); } + private function setProperties(object $object, \IteratorAggregate $bag, bool $override) { + foreach ($bag as $key => $value) { + if(false === $override && property_exists($object, $key)) { + continue; + } + $object->{$key} = $value; + } + } + private function getReflectionMethod(callable $controller): \ReflectionMethod { if (is_array($controller)) { @@ -85,4 +106,14 @@ private function getReflectionMethod(callable $controller): \ReflectionMethod return new \ReflectionMethod($class, $method); } + + private function getSerializer(): SerializerInterface + { + if (!class_exists(SerializerInterface::class)) { + throw new LogicException(sprintf('The "symfony/serializer" component is required to use the "%s" validator. Try running "composer require symfony/serializer".', + __CLASS__)); + } + + return $this->serializer; + } } From aa69324081232e76f53b0bcf78ec3fff49d2826f Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Sat, 21 Jan 2023 14:57:39 +0100 Subject: [PATCH 3/4] fixing some bugs in requestValidation --- .../Validator/Attribute/RequestValidator.php | 2 +- .../RequestValidationSubscriber.php | 44 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Validator/Attribute/RequestValidator.php b/src/Symfony/Component/Validator/Attribute/RequestValidator.php index ef37b6b300707..3ba871036947c 100644 --- a/src/Symfony/Component/Validator/Attribute/RequestValidator.php +++ b/src/Symfony/Component/Validator/Attribute/RequestValidator.php @@ -2,7 +2,7 @@ namespace Symfony\Component\Validator\Attribute; -#[\Attribute(\Attribute::TARGET_FUNCTION)] +#[\Attribute(\Attribute::TARGET_METHOD)] final class RequestValidator { public const ORDER_ATTRIBUTES = 'attributes'; diff --git a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php index 0552e20bbce85..3c853c54ee283 100644 --- a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php +++ b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php @@ -5,6 +5,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Attribute\RequestValidator; use Symfony\Component\Validator\Exception\LogicException; @@ -41,28 +42,49 @@ public function validateRequest(ControllerArgumentsEvent $event): void { // only first attribute can validate $attribute = $attributes[0]; - $class = $attribute->getArguments()['class']; - $override = $attribute->getArguments()['override']; - $serializedFormat = $attribute->getArguments()['serializedFormat']; - $order = $attribute->getArguments()['order']; + $attributeArguments = $attribute->getArguments(); + if(key_exists('class', $attributeArguments)) { + $class = $attributeArguments['class']; + $override = key_exists('override', $attributeArguments) ? $attributeArguments['override'] : true; + $order = key_exists('order', $attributeArguments) ? $attributeArguments['order'] : [ + RequestValidator::ORDER_SERIALIZE, + RequestValidator::ORDER_ATTRIBUTES, + RequestValidator::ORDER_QUERY, + RequestValidator::ORDER_REQUEST, + ]; + $serializedFormat = key_exists('serializedFormat', $attributeArguments) ? $attributeArguments['json'] : 'json'; + }else { + $class = $attributeArguments[0]; + $override = key_exists(1, $attributeArguments) ? $attributeArguments[1] : true; + $order = key_exists(2, $attributeArguments) ? $attributeArguments[2] : [ + RequestValidator::ORDER_SERIALIZE, + RequestValidator::ORDER_ATTRIBUTES, + RequestValidator::ORDER_QUERY, + RequestValidator::ORDER_REQUEST, + ]; + $serializedFormat = key_exists(3, $attributeArguments) ? $attributeArguments[3] : 'json'; + } $object = new $class(); foreach ($order as $type) { switch ($type) { case RequestValidator::ORDER_SERIALIZE: + if(empty($request->getContent())) { + continue 2; + } $serializer = $this->getSerializer(); $serializer->deserialize($request->getContent(), $class, $serializedFormat, [AbstractNormalizer::OBJECT_TO_POPULATE => $object]); continue 2; case RequestValidator::ORDER_REQUEST: - $this->setProperties($object, $request->request, $override); + $this->setProperties($object, $request->request->all(), $override); break; case RequestValidator::ORDER_QUERY: - $this->setProperties($object, $request->query, $override); + $this->setProperties($object, $request->query->all(), $override); break; case RequestValidator::ORDER_ATTRIBUTES: - $this->setProperties($object, $request->attributes, $override); + $this->setProperties($object, $request->attributes->all(), $override); break; } @@ -84,9 +106,9 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $event->setArguments($arguments); } - private function setProperties(object $object, \IteratorAggregate $bag, bool $override) { - foreach ($bag as $key => $value) { - if(false === $override && property_exists($object, $key)) { + private function setProperties(object $object, array $parameters, bool $override) { + foreach ($parameters as $key => $value) { + if(false === $override && property_exists($object, $key) && isset($object->{$key})) { continue; } $object->{$key} = $value; @@ -109,7 +131,7 @@ private function getReflectionMethod(callable $controller): \ReflectionMethod private function getSerializer(): SerializerInterface { - if (!class_exists(SerializerInterface::class)) { + if (!class_exists(Serializer::class)) { throw new LogicException(sprintf('The "symfony/serializer" component is required to use the "%s" validator. Try running "composer require symfony/serializer".', __CLASS__)); } From 5b12b32a4d60a2812105ccea6e3bf5443fd55711 Mon Sep 17 00:00:00 2001 From: Thomas Hanke Date: Sat, 21 Jan 2023 21:22:38 +0100 Subject: [PATCH 4/4] remove serializer form default --- src/Symfony/Component/Validator/Attribute/RequestValidator.php | 1 - .../Validator/EventListener/RequestValidationSubscriber.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Attribute/RequestValidator.php b/src/Symfony/Component/Validator/Attribute/RequestValidator.php index 3ba871036947c..d949792863170 100644 --- a/src/Symfony/Component/Validator/Attribute/RequestValidator.php +++ b/src/Symfony/Component/Validator/Attribute/RequestValidator.php @@ -14,7 +14,6 @@ public function __construct( public string $class, public bool $override = true, public array $order = [ - self::ORDER_SERIALIZE, self::ORDER_ATTRIBUTES, self::ORDER_QUERY, self::ORDER_REQUEST, diff --git a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php index 3c853c54ee283..1f7a2e341cafd 100644 --- a/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php +++ b/src/Symfony/Component/Validator/EventListener/RequestValidationSubscriber.php @@ -47,7 +47,6 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $class = $attributeArguments['class']; $override = key_exists('override', $attributeArguments) ? $attributeArguments['override'] : true; $order = key_exists('order', $attributeArguments) ? $attributeArguments['order'] : [ - RequestValidator::ORDER_SERIALIZE, RequestValidator::ORDER_ATTRIBUTES, RequestValidator::ORDER_QUERY, RequestValidator::ORDER_REQUEST, @@ -57,7 +56,6 @@ public function validateRequest(ControllerArgumentsEvent $event): void { $class = $attributeArguments[0]; $override = key_exists(1, $attributeArguments) ? $attributeArguments[1] : true; $order = key_exists(2, $attributeArguments) ? $attributeArguments[2] : [ - RequestValidator::ORDER_SERIALIZE, RequestValidator::ORDER_ATTRIBUTES, RequestValidator::ORDER_QUERY, RequestValidator::ORDER_REQUEST,