diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php index c397e73d42505..98dbe13590cd4 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\ControllerArgumentConstraintAttributeListener; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -26,7 +27,8 @@ return static function (ContainerConfigurator $container) { $container->parameters() - ->set('validator.mapping.cache.file', param('kernel.cache_dir').'/validation.php'); + ->set('validator.mapping.cache.file', param('kernel.cache_dir').'/validation.php') + ; $container->services() ->set('validator', ValidatorInterface::class) @@ -109,5 +111,11 @@ service('property_info'), ]) ->tag('validator.auto_mapper') + + ->set('controller.controller_argument_constraint_attribute_listener', ControllerArgumentConstraintAttributeListener::class) + ->args([ + service('validator'), + ]) + ->tag('kernel.event_subscriber') ; }; diff --git a/src/Symfony/Component/Validator/Constraints/ControllerArgument.php b/src/Symfony/Component/Validator/Constraints/ControllerArgument.php new file mode 100644 index 0000000000000..4dd4142ce1359 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/ControllerArgument.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +/** + * @author Dynèsh Hassanaly + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class ControllerArgument extends Composite +{ + public $constraints = []; + + public function __construct(mixed $constraints = null, array $groups = null, mixed $payload = null) + { + parent::__construct($constraints ?? [], $groups, $payload); + } + + public function getDefaultOption(): ?string + { + return 'constraints'; + } + + public function getRequiredOptions(): array + { + return ['constraints']; + } + + /** + * @inheritDoc + */ + protected function getCompositeOption(): string + { + return 'constraints'; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/ControllerArgumentValidator.php b/src/Symfony/Component/Validator/Constraints/ControllerArgumentValidator.php new file mode 100644 index 0000000000000..696b289875914 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/ControllerArgumentValidator.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Dynèsh Hassanaly + */ +class ControllerArgumentValidator extends ConstraintValidator +{ + /** + * @inheritDoc + */ + public function validate(mixed $value, Constraint $constraint) + { + if (!$constraint instanceof ControllerArgument) { + throw new UnexpectedTypeException($constraint, ControllerArgument::class); + } + + if (null === $value) { + return; + } + + $context = $this->context; + $validator = $context->getValidator()->inContext($context); + + foreach ($constraint->constraints as $c) { + $validator->validate($value, $c); + } + } +} diff --git a/src/Symfony/Component/Validator/EventListener/ControllerArgumentConstraintAttributeListener.php b/src/Symfony/Component/Validator/EventListener/ControllerArgumentConstraintAttributeListener.php new file mode 100644 index 0000000000000..b5d2e1b559b25 --- /dev/null +++ b/src/Symfony/Component/Validator/EventListener/ControllerArgumentConstraintAttributeListener.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\ControllerArgument; +use Symfony\Component\Validator\Exception\ValidationFailedException; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Handles the validator constraint attributes on controller's arguments. + * + * @author Dynèsh Hassanaly + */ +class ControllerArgumentConstraintAttributeListener implements EventSubscriberInterface +{ + public function __construct(private readonly ValidatorInterface $validator) {} + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + $controller = $event->getController(); + $arguments = $event->getArguments(); + $reflectionMethod = $this->getReflectionMethod($controller); + + foreach ($reflectionMethod->getParameters() as $index => $reflectionParameter) { + $reflectionAttributes = $reflectionParameter->getAttributes(ControllerArgument::class); + + if (!$reflectionAttributes) { + continue; + } + + // this attribute cannot be repeated, so we will always one item in this array + $reflectionAttribute = $reflectionAttributes[0]; + + /** @var Constraint $constraint */ + $constraint = $reflectionAttribute->newInstance(); + $value = $arguments[$index]; + $violations = $this->validator->validate($value, $constraint); + + if ($violations->count() > 0) { + throw new ValidationFailedException($value, $violations); + } + } + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 10]]; + } + + 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); + } +}