diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 961711fd28cad..3fb334b0763b7 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -56,6 +56,7 @@ public function __construct() new RegisterServiceSubscribersPass(), new ResolveParameterPlaceHoldersPass(false, false), new ResolveFactoryClassPass(), + new ValidateConstructorArgumentsPass(), new ResolveNamedArgumentsPass(), new AutowireRequiredMethodsPass(), new AutowireRequiredPropertiesPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ValidateConstructorArgumentsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ValidateConstructorArgumentsPass.php new file mode 100644 index 0000000000000..6a0976a3e3430 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ValidateConstructorArgumentsPass.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; +use Symfony\Component\Validator\Exception\ValidationFailedException; +use Symfony\Component\Validator\Mapping\Loader\AbstractLoader; +use Symfony\Component\Validator\Validation; + +/** + * Validates service arguments using Validator component. + */ +final class ValidateConstructorArgumentsPass extends AbstractRecursivePass +{ + /** @var bool */ + private $throwExceptionOnValidationFailure; + + public function __construct(bool $throwExceptionOnValidationFailure = true) + { + $this->throwExceptionOnValidationFailure = $throwExceptionOnValidationFailure; + } + + /** + * {@inheritdoc} + */ + protected function processValue($value, bool $isRoot = false) + { + if (!$value instanceof Definition || $value->hasErrors()) { + return parent::processValue($value, $isRoot); + } + + if (ServiceLocator::class === $value->getClass()) { + return parent::processValue($value, $isRoot); + } + + if (\count($value->getConstraints()) > 0) { + $this->validate($value); + } + + return parent::processValue($value, $isRoot); + } + + private function validate(Definition $value): void + { + $serviceConstraints = $value->getConstraints(); + foreach ($serviceConstraints as $argumentName => $argumentConstraints) { + $argumentValue = $value->getArgument($argumentName); + + $validatorConstraints = $this->getValidatorConstraints($argumentConstraints); + $validator = Validation::createCallable(null, ...$validatorConstraints); + try { + $validator($argumentValue); + } catch (ValidationFailedException $e) { + if ($this->throwExceptionOnValidationFailure) { + throw $e; + } + + $value->addError($e); + } + } + } + + /** + * @param mixed[] $rawConstraints Constraints definition, parsed from config file + * + * @return Constraint[] + */ + private function getValidatorConstraints(array $rawConstraints): array + { + $constraintsList = []; + foreach ($rawConstraints as $constraintName => $constraintValue) { + $validatorConstraintClass = AbstractLoader::DEFAULT_NAMESPACE.$constraintName; + + if (is_subclass_of($validatorConstraintClass, Composite::class)) { + $constraintValue = $this->getValidatorConstraints($constraintValue); + } + + $constraintsList[] = new $validatorConstraintClass($constraintValue); + } + + return $constraintsList; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 3974eefdda0d2..41b1d4a5a4cef 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -44,6 +44,7 @@ class Definition private $errors = []; protected $arguments = []; + protected $constraints = []; private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.'; @@ -324,6 +325,63 @@ public function getArgument($index) return $this->arguments[$index]; } + /** + * Sets constraints to validate arguments. + * + * @param mixed[] $constraints + * + * @return $this + */ + public function setConstraints(array $constraints) + { + $this->constraints = $constraints; + + return $this; + } + + /** + * Gets constraints list to validate arguments. + * + * @return mixed[] The array of constraints + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Sets specific constraints for argument. + * + * @param int|string $key + * @param mixed $value + * + * @return $this + */ + public function setConstraint($key, $value) + { + $this->constraints[$key] = $value; + + return $this; + } + + /** + * Gets constraints to validate argument. + * + * @param int|string $index + * + * @return mixed The arguments constraints + * + * @throws OutOfBoundsException When the constraint does not exist + */ + public function getConstraint($index) + { + if (!\array_key_exists($index, $this->constraints)) { + throw new OutOfBoundsException(sprintf('The constraint "%s" doesn\'t exist.', $index)); + } + + return $this->constraints[$index]; + } + /** * Sets the methods to call after service initialization. * diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 61b4c5fa6ce7f..8b40f0b350a90 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -125,6 +125,10 @@ private function addService(string $id, Definition $definition): string $code .= sprintf(" arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0)); } + if ($definition->getConstraints()) { + $code .= sprintf(" constraints: %s\n", $this->dumper->dump($this->dumpValue($definition->getConstraints()), 0)); + } + if ($definition->getProperties()) { $code .= sprintf(" properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0)); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 618f2bfc73f0d..56333f38f5c3b 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -63,6 +63,7 @@ class YamlFileLoader extends FileLoader 'autowire' => 'autowire', 'autoconfigure' => 'autoconfigure', 'bind' => 'bind', + 'constraints' => 'constraints', ]; private static $prototypeKeywords = [ @@ -500,6 +501,10 @@ private function parseDefinition(string $id, $service, string $file, array $defa $definition->setArguments($this->resolveServices($service['arguments'], $file)); } + if (isset($service['constraints'])) { + $definition->setConstraints($this->resolveServices($service['constraints'], $file)); + } + if (isset($service['properties'])) { $definition->setProperties($this->resolveServices($service['properties'], $file)); } @@ -868,7 +873,7 @@ private function resolveServices($value, string $file, bool $isParameter = false } } elseif (\is_string($value) && 0 === strpos($value, '@=')) { if (!class_exists(Expression::class)) { - throw new \LogicException(sprintf('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".')); + throw new \LogicException('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'); } return new Expression(substr($value, 2)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateConstructorArgumentsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateConstructorArgumentsPassTest.php new file mode 100644 index 0000000000000..b63360cef8bbb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ValidateConstructorArgumentsPassTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\ValidateConstructorArgumentsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Validator\Constraints\Ip; +use Symfony\Component\Validator\Exception\ValidationFailedException; + +class ValidateConstructorArgumentsPassTest extends TestCase +{ + public function testValidationSuccess() + { + $container = new ContainerBuilder(); + $definition = $container->register('service', \stdClass::class); + $definition + ->setArguments([ + '$int' => 1, + '$array' => [1, 2, 3], + '$email' => 'test@email.com', + '$datetime' => '2020-12-31 23:59:59', + '$ipAddresses' => ['8.8.4.4', '8.8.8.8'], + '$noConstraints' => 'no constraints for this argument', + ]) + ->setConstraints([ + '$int' => ['EqualTo' => 1], + '$array' => [ + 'Count' => [ + 'min' => 1, + 'max' => 5, + ], + ], + '$email' => ['Email' => null], + '$datetime' => [ + 'NotBlank' => null, + 'DateTime' => null, + ], + '$ipAddresses' => [ + 'All' => [ + 'NotBlank' => null, + 'Ip' => ['version' => Ip::V4_ONLY_PUBLIC], + ], + ], + ]); + + $pass = new ValidateConstructorArgumentsPass(false); + $pass->process($container); + + $this->assertCount(0, $definition->getErrors()); + } + + public function testValidationFailedWithThrowExceptionOnFailure() + { + $this->expectException(ValidationFailedException::class); + $this->expectExceptionMessage('Provided string does not look like JSON. (code 0789c8ad-2d2b-49a4-8356-e2ce63998504)'); + + $container = new ContainerBuilder(); + $definition = $container->register('service', \stdClass::class); + $definition + ->setArguments([ + '$json' => 'wrong json', + ]) + ->setConstraints([ + '$json' => [ + 'Json' => ['message' => 'Provided string does not look like JSON.'], + ], + ]); + + $pass = new ValidateConstructorArgumentsPass(); + $pass->process($container); + } + + public function testValidationFailedWithDoNotThrowExceptionOnFailure() + { + $container = new ContainerBuilder(); + $definition = $container->register('service', \stdClass::class); + $definition + ->setArguments([ + '$choice' => 'foo', + ]) + ->setConstraints([ + '$choice' => [ + 'Choice' => [ + 'choices' => ['bar', 'baz'], + 'message' => 'Choice should be one of: bar, baz.', + ], + ], + ]); + + $pass = new ValidateConstructorArgumentsPass(false); + $pass->process($container); + + $this->assertCount(1, $definition->getErrors()); + $this->assertMatchesRegularExpression( + '/Choice should be one of: bar, baz. \(code 8e179f1b-97aa-4560-a02f-2a8b42e49df7\)/', + $definition->getErrors()[0] + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php index b9a1903beab54..107ed2c0a88db 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_duplicates.php @@ -42,7 +42,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.mtT6G8y' => true, + '.service_locator.yG6Rg7I' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'foo' => true, diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php index 0bb94cf01ff74..dfc2525afce02 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php @@ -44,7 +44,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.PWbaRiJ' => true, + '.service_locator.k59fPaB' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, ]; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php index 70a20fc6cc230..e967d4dce9740 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_service_locator_argument.php @@ -45,7 +45,7 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.ZP1tNYN' => true, + '.service_locator.wX0ALtJ' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'foo2' => true, diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index 9bca4ed5578de..bcb736056546a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -44,8 +44,8 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.DlIAmAe' => true, - '.service_locator.DlIAmAe.foo_service' => true, + '.service_locator.u.4vYl9' => true, + '.service_locator.u.4vYl9.foo_service' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, 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