diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php index c397e73d42505..6eaafb704afdc 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('validator'), + service('serializer')->nullOnInvalid(), + ]) + ->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..d949792863170 --- /dev/null +++ b/src/Symfony/Component/Validator/Attribute/RequestValidator.php @@ -0,0 +1,24 @@ + '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]; + + $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_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_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->all(), $override); + break; + case RequestValidator::ORDER_QUERY: + $this->setProperties($object, $request->query->all(), $override); + break; + case RequestValidator::ORDER_ATTRIBUTES: + $this->setProperties($object, $request->attributes->all(), $override); + break; + } + + } + + $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 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; + } + } + + 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); + } + + private function getSerializer(): SerializerInterface + { + 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__)); + } + + return $this->serializer; + } +}