From a6292b917b22c44625f1403c3b4742d908ffc044 Mon Sep 17 00:00:00 2001 From: Julien Maulny Date: Fri, 29 Jun 2018 16:18:59 +0200 Subject: [PATCH 1/3] [DI] Add compiler pass to check arguments type hint --- .../Command/ContainerLintCommand.php | 87 +++ .../Resources/config/console.xml | 4 + .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/CheckTypeHintsPass.php | 184 ++++++ .../InvalidParameterTypeHintException.php | 28 + .../Tests/Compiler/CheckTypeHintsPassTest.php | 578 ++++++++++++++++++ .../Tests/Fixtures/CheckTypeHintsPass/Bar.php | 13 + .../CheckTypeHintsPass/BarMethodCall.php | 35 ++ .../BarOptionalArgument.php | 13 + .../BarOptionalArgumentNotNull.php | 13 + .../Tests/Fixtures/CheckTypeHintsPass/Foo.php | 16 + 11 files changed, 972 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarMethodCall.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgumentNotNull.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Foo.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php new file mode 100644 index 0000000000000..c7c6cb45071c8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Config\ConfigCache; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeHintsPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\Config\FileLocator; + +class ContainerLintCommand extends Command +{ + /** + * @var ContainerBuilder + */ + private $containerBuilder; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDescription('Lints container for services arguments type hints') + ->setHelp('This command will parse all your defined services and check that you are injecting service without type error based on type hints.') + ->addOption('only-used-services', 'o', InputOption::VALUE_NONE, 'Check only services that are used in your application') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $container = $this->getContainerBuilder(); + + $container->setParameter('container.build_id', 'lint_container'); + + $container->addCompilerPass( + new CheckTypeHintsPass(), + $input->getOption('only-used-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_BEFORE_OPTIMIZATION + ); + + $container->compile(); + } + + /** + * Loads the ContainerBuilder from the cache. + * + * @return ContainerBuilder + * + * @throws \LogicException + */ + protected function getContainerBuilder() + { + if ($this->containerBuilder) { + return $this->containerBuilder; + } + + $kernel = $this->getApplication()->getKernel(); + + if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { + $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, get_class($kernel)); + $container = $buildContainer(); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + } else { + (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); + } + + return $this->containerBuilder = $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index f13aa759d31cd..eff7ec6694c85 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -70,6 +70,10 @@ + + + + null diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index c90cfa747128f..7da5b6b534b64 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.4.0 ----- + * added `CheckTypeHintsPass` to check injected parameters type during compilation * added support for opcache.preload by generating a preloading script in the cache folder * added support for dumping the container in one file instead of many files * deprecated support for short factories and short configurators in Yaml diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php new file mode 100644 index 0000000000000..41085b8e68848 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php @@ -0,0 +1,184 @@ + + * + * 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\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeHintException; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; + +/** + * Checks whether injected parameters types are compatible with type hints. + * This pass should be run after all optimization passes. + * So it can be added either: + * * before removing (PassConfig::TYPE_BEFORE_REMOVING) so that it will check + * all services, even if they are not currently used, + * * after removing (PassConfig::TYPE_AFTER_REMOVING) so that it will check + * only services you are using. + * + * @author Nicolas Grekas + * @author Julien Maulny + */ +class CheckTypeHintsPass extends AbstractRecursivePass +{ + /** + * If set to true, allows to autoload classes during compilation + * in order to check type hints on parameters that are not yet loaded. + * Defaults to false to prevent code loading during compilation. + * + * @param bool + */ + private $autoload; + + public function __construct(bool $autoload = false) + { + $this->autoload = $autoload; + } + + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + + if (!$this->autoload && !class_exists($className = $this->getClassName($value), false) && !interface_exists($className, false)) { + return parent::processValue($value, $isRoot); + } + + if (ServiceLocator::class === $value->getClass()) { + return parent::processValue($value, $isRoot); + } + + if (null !== $constructor = $this->getConstructor($value, false)) { + $this->checkArgumentsTypeHints($constructor, $value->getArguments()); + } + + foreach ($value->getMethodCalls() as $methodCall) { + $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]); + + $this->checkArgumentsTypeHints($reflectionMethod, $methodCall[1]); + } + + return parent::processValue($value, $isRoot); + } + + /** + * Check type hints for every parameter of a method/constructor. + * + * @throws InvalidArgumentException on type hint incompatibility + */ + private function checkArgumentsTypeHints(\ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void + { + $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters(); + + if (count($configurationArguments) < $numberOfRequiredParameters) { + throw new InvalidArgumentException(sprintf( + 'Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, count($configurationArguments))); + } + + $reflectionParameters = $reflectionFunction->getParameters(); + $checksCount = min($reflectionFunction->getNumberOfParameters(), count($configurationArguments)); + + for ($i = 0; $i < $checksCount; ++$i) { + if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) { + continue; + } + + $this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]); + } + + if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) { + $variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition()); + + foreach ($variadicParameters as $variadicParameter) { + $this->checkTypeHint($variadicParameter, $lastParameter); + } + } + } + + /** + * Check type hints compatibility between + * a definition argument and a reflection parameter. + * + * @throws InvalidArgumentException on type hint incompatibility + */ + private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter): void + { + $referencedDefinition = $configurationArgument; + + if ($referencedDefinition instanceof Reference) { + $referencedDefinition = $this->container->findDefinition((string) $referencedDefinition); + } + + if ($referencedDefinition instanceof Definition) { + $class = $this->getClassName($referencedDefinition); + + if (!$this->autoload && !class_exists($class, false)) { + return; + } + + if (!is_a($class, $parameter->getType()->getName(), true)) { + throw new InvalidParameterTypeHintException($this->currentId, null === $class ? 'null' : $class, $parameter); + } + } else { + if (null === $configurationArgument && $parameter->allowsNull()) { + return; + } + + if ($parameter->getType()->isBuiltin() && is_scalar($configurationArgument)) { + return; + } + + if ('iterable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) { + return; + } + + if ('Traversable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) { + return; + } + + $checkFunction = 'is_'.$parameter->getType()->getName(); + + if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) { + throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter); + } + } + } + + /** + * Get class name from value that can have a factory. + * + * @return string|null + */ + private function getClassName($value) + { + if (is_array($factory = $value->getFactory())) { + list($class, $method) = $factory; + if ($class instanceof Reference) { + $class = $this->container->findDefinition((string) $class)->getClass(); + } elseif (null === $class) { + $class = $value->getClass(); + } elseif ($class instanceof Definition) { + $class = $this->getClassName($class); + } + } else { + $class = $value->getClass(); + } + + return $class; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php new file mode 100644 index 0000000000000..a04d0719729bc --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * Thrown when trying to inject a parameter into a constructor/method + * with a type that does not match type hint. + * + * @author Nicolas Grekas + * @author Julien Maulny + */ +class InvalidParameterTypeHintException extends InvalidArgumentException +{ + public function __construct(string $serviceId, string $typeHint, \ReflectionParameter $parameter) + { + parent::__construct(sprintf( + 'Invalid definition for service "%s": argument %d of "%s::%s" requires a "%s", "%s" passed.', $serviceId, $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $typeHint)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php new file mode 100644 index 0000000000000..690c628cb64a9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php @@ -0,0 +1,578 @@ + + * + * 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\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeHintsPass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgument; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgumentNotNull; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo; + +/** + * @author Nicolas Grekas + * @author Julien Maulny + */ +class CheckTypeHintsPassTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed + */ + public function testProcessThrowsExceptionOnInvalidTypeHintsConstructorArguments() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('foo')); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed + */ + public function testProcessThrowsExceptionOnInvalidTypeHintsMethodCallArguments() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoo', array(new Reference('foo'))); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "NULL" passed + */ + public function testProcessFailsWhenPassingNullToRequiredArgument() + { + $container = new ContainerBuilder(); + + $container->register('bar', Bar::class) + ->addArgument(null); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct()" requires 1 arguments, 0 passed + */ + public function testProcessThrowsExceptionWhenMissingArgumentsInConstructor() + { + $container = new ContainerBuilder(); + + $container->register('bar', Bar::class); + + (new CheckTypeHintsPass(true))->process($container); + } + + public function testProcessSuccessWhenPassingTooManyArgumentInConstructor() + { + $container = new ContainerBuilder(); + + $container->register('foo', \stdClass::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('foo')) + ->addArgument(new Reference('foo')); + + (new CheckTypeHintsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessRegisterWithClassName() + { + $container = new ContainerBuilder(); + + $container->register(Foo::class, Foo::class); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Foo::class, $container->get(Foo::class)); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo()" requires 1 arguments, 0 passed + */ + public function testProcessThrowsExceptionWhenMissingArgumentsInMethodCall() + { + $container = new ContainerBuilder(); + + $container->register('foo', \stdClass::class); + $container->register('bar', BarMethodCall::class) + ->addArgument(new Reference('foo')) + ->addMethodCall('setFoo', array()); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosVariadic" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed + */ + public function testProcessVariadicFails() + { + $container = new ContainerBuilder(); + + $container->register('stdClass', \stdClass::class); + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosVariadic', array( + new Reference('foo'), + new Reference('foo'), + new Reference('stdClass'), + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosVariadic" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed + */ + public function testProcessVariadicFailsOnPassingBadTypeOnAnotherArgument() + { + $container = new ContainerBuilder(); + + $container->register('stdClass', \stdClass::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosVariadic', array( + new Reference('stdClass'), + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + public function testProcessVariadicSuccess() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosVariadic', array( + new Reference('foo'), + new Reference('foo'), + new Reference('foo'), + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); + } + + public function testProcessSuccessWhenNotUsingOptionalArgument() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosOptional', array( + new Reference('foo'), + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); + } + + public function testProcessSuccessWhenUsingOptionalArgumentWithGoodType() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosOptional', array( + new Reference('foo'), + new Reference('foo'), + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosOptional" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed + */ + public function testProcessFailsWhenUsingOptionalArgumentWithBadType() + { + $container = new ContainerBuilder(); + + $container->register('stdClass', \stdClass::class); + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoosOptional', array( + new Reference('foo'), + new Reference('stdClass'), + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + public function testProcessSuccessWhenPassingNullToOptional() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarOptionalArgument::class) + ->addArgument(null); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertNull($container->get('bar')->foo); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgumentNotNull::__construct" requires a "int", "NULL" passed + */ + public function testProcessSuccessWhenPassingNullToOptionalThatDoesNotAcceptNull() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarOptionalArgumentNotNull::class) + ->addArgument(null); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgument::__construct" requires a "stdClass", "string" passed + */ + public function testProcessFailsWhenPassingBadTypeToOptional() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarOptionalArgument::class) + ->addArgument('string instead of stdClass'); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertNull($container->get('bar')->foo); + } + + public function testProcessSuccessScalarType() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setScalars', array( + 1, + 'string', + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "integer" passed + */ + public function testProcessFailsOnPassingScalarTypeToConstructorTypeHintedWithClass() + { + $container = new ContainerBuilder(); + + $container->register('bar', Bar::class) + ->addArgument(1); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo" requires a "stdClass", "string" passed + */ + public function testProcessFailsOnPassingScalarTypeToMethodTypeHintedWithClass() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setFoo', array( + 'builtin type instead of class', + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setScalars" requires a "int", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed + */ + public function testProcessFailsOnPassingClassToScalarTypeHintedParameter() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setScalars', array( + new Reference('foo'), + new Reference('foo'), + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * Strict mode not yet handled. + */ + public function testProcessSuccessOnPassingBadScalarType() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setScalars', array( + 1, + true, + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); + } + + /** + * Strict mode not yet handled. + */ + public function testProcessSuccessPassingBadScalarTypeOptionalArgument() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setScalars', array( + 1, + 'string', + 'string instead of optional boolean', + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); + } + + public function testProcessSuccessWhenPassingArray() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setArray', array( + array(), + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); + } + + public function testProcessSuccessWhenPassingIntegerToArrayTypeHintedParameter() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setArray', array( + 1, + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessSuccessWhenPassingAnIteratorArgumentToIterable() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setIterable', array( + new IteratorArgument(array()), + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessFactory() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', Bar::class) + ->setFactory(array( + new Reference('foo'), + 'createBar', + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Bar::class, $container->get('bar')); + } + + public function testProcessFactoryWhithClassName() + { + $container = new ContainerBuilder(); + + $container->register(Foo::class, Foo::class); + $container->register(Bar::class, Bar::class) + ->setFactory(array( + new Reference(Foo::class), + 'createBar', + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(Bar::class, $container->get(Bar::class)); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo::createBarArguments" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed + */ + public function testProcessFactoryFailsOnInvalidParameterType() + { + $container = new ContainerBuilder(); + + $container->register('foo', Foo::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('foo')) + ->setFactory(array( + new Reference('foo'), + 'createBarArguments', + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo::createBarArguments" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed + */ + public function testProcessFactoryFailsOnInvalidParameterTypeOptional() + { + $container = new ContainerBuilder(); + + $container->register('stdClass', \stdClass::class); + $container->register('foo', Foo::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('stdClass')) + ->addArgument(new Reference('foo')) + ->setFactory(array( + new Reference('foo'), + 'createBarArguments', + )); + + (new CheckTypeHintsPass(true))->process($container); + } + + public function testProcessFactorySuccessOnValidTypes() + { + $container = new ContainerBuilder(); + + $container->register('stdClass', \stdClass::class); + $container->register('foo', Foo::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('stdClass')) + ->addArgument(new Reference('stdClass')) + ->setFactory(array( + new Reference('foo'), + 'createBarArguments', + )); + + (new CheckTypeHintsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessFactoryCallbackSuccessOnValidType() + { + $container = new ContainerBuilder(); + + $container->register('bar', \DateTime::class) + ->setFactory('date_create'); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(\DateTime::class, $container->get('bar')); + } + + public function testProcessDoesNotLoadCodeByDefault() + { + $container = new ContainerBuilder(); + + $container->register('foo', FooNotExisting::class); + $container->register('bar', BarNotExisting::class) + ->addArgument(new Reference('foo')) + ->addMethodCall('setFoo', array( + new Reference('foo'), + 'string', + 1, + )); + + (new CheckTypeHintsPass())->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessFactoryDoesNotLoadCodeByDefault() + { + $container = new ContainerBuilder(); + + $container->register('foo', FooNotExisting::class); + $container->register('bar', BarNotExisting::class) + ->setFactory(array( + new Reference('foo'), + 'notExistingMethod', + )); + + (new CheckTypeHintsPass())->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessPassingBuiltinTypeDoesNotLoadCodeByDefault() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarNotExisting::class) + ->addArgument(1); + + (new CheckTypeHintsPass())->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessDoesNotThrowsExceptionOnValidTypeHints() + { + $container = new ContainerBuilder(); + + $container->register('foo', \stdClass::class); + $container->register('bar', Bar::class) + ->addArgument(new Reference('foo')); + + (new CheckTypeHintsPass(true))->process($container); + + $this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php new file mode 100644 index 0000000000000..85a1289815f85 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php @@ -0,0 +1,13 @@ +foo = $foo; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarMethodCall.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarMethodCall.php new file mode 100644 index 0000000000000..076c219d3a4e0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarMethodCall.php @@ -0,0 +1,35 @@ +foo = $foo; + } + + public function setFoosVariadic(Foo $foo, Foo ...$foos) + { + $this->foo = $foo; + } + + public function setFoosOptional(Foo $foo, Foo $fooOptional = null) + { + $this->foo = $foo; + } + + public function setScalars(int $int, string $string, bool $bool = false) + { + } + + public function setArray(array $array) + { + } + + public function setIterable(iterable $iterable) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php new file mode 100644 index 0000000000000..3b6daa77f84dd --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php @@ -0,0 +1,13 @@ +foo = $foo; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgumentNotNull.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgumentNotNull.php new file mode 100644 index 0000000000000..5e54114aef90e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgumentNotNull.php @@ -0,0 +1,13 @@ +foo = $foo; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Foo.php new file mode 100644 index 0000000000000..9132d6a246327 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Foo.php @@ -0,0 +1,16 @@ + Date: Fri, 28 Jun 2019 14:16:44 +0200 Subject: [PATCH 2/3] Fix comments, improve the feature --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/ContainerLintCommand.php | 43 ++- .../DependencyInjection/CHANGELOG.md | 2 +- .../Compiler/CheckTypeDeclarationsPass.php | 172 +++++++++ .../Compiler/CheckTypeHintsPass.php | 184 ---------- .../InvalidParameterTypeException.php | 26 ++ .../InvalidParameterTypeHintException.php | 28 -- ....php => CheckTypeDeclarationsPassTest.php} | 333 ++++++++---------- .../Bar.php | 2 +- .../BarMethodCall.php | 6 +- .../BarOptionalArgument.php | 2 +- .../BarOptionalArgumentNotNull.php | 2 +- .../Foo.php | 2 +- 13 files changed, 384 insertions(+), 419 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php delete mode 100644 src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php delete mode 100644 src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php rename src/Symfony/Component/DependencyInjection/Tests/Compiler/{CheckTypeHintsPassTest.php => CheckTypeDeclarationsPassTest.php} (50%) rename src/Symfony/Component/DependencyInjection/Tests/Fixtures/{CheckTypeHintsPass => CheckTypeDeclarationsPass}/Bar.php (88%) rename src/Symfony/Component/DependencyInjection/Tests/Fixtures/{CheckTypeHintsPass => CheckTypeDeclarationsPass}/BarMethodCall.php (89%) rename src/Symfony/Component/DependencyInjection/Tests/Fixtures/{CheckTypeHintsPass => CheckTypeDeclarationsPass}/BarOptionalArgument.php (89%) rename src/Symfony/Component/DependencyInjection/Tests/Fixtures/{CheckTypeHintsPass => CheckTypeDeclarationsPass}/BarOptionalArgumentNotNull.php (89%) rename src/Symfony/Component/DependencyInjection/Tests/Fixtures/{CheckTypeHintsPass => CheckTypeDeclarationsPass}/Foo.php (92%) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 093f4bb1da1a0..b8508809ae9cd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.4.0 ----- + * Added `lint:container` to check that services wiring matches type declarations * Added `MailerAssertionsTrait` * Deprecated support for `templating` engine in `TemplateController`, use Twig instead * Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index c7c6cb45071c8..04ecff767f5f3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -11,19 +11,21 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Config\ConfigCache; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Compiler\CheckTypeHintsPass; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; -use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -class ContainerLintCommand extends Command +final class ContainerLintCommand extends Command { + protected static $defaultName = 'lint:container'; + /** * @var ContainerBuilder */ @@ -35,37 +37,33 @@ class ContainerLintCommand extends Command protected function configure() { $this - ->setDescription('Lints container for services arguments type hints') - ->setHelp('This command will parse all your defined services and check that you are injecting service without type error based on type hints.') - ->addOption('only-used-services', 'o', InputOption::VALUE_NONE, 'Check only services that are used in your application') + ->setDescription('Ensures that arguments injected into services match type declarations') + ->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.') + ->addOption('ignore-unused-services', 'o', InputOption::VALUE_NONE, 'Ignore unused services') ; } /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $container = $this->getContainerBuilder(); $container->setParameter('container.build_id', 'lint_container'); $container->addCompilerPass( - new CheckTypeHintsPass(), - $input->getOption('only-used-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_BEFORE_OPTIMIZATION + new CheckTypeDeclarationsPass(true), + $input->getOption('ignore-unused-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_OPTIMIZE, + -5 ); $container->compile(); + + return 0; } - /** - * Loads the ContainerBuilder from the cache. - * - * @return ContainerBuilder - * - * @throws \LogicException - */ - protected function getContainerBuilder() + private function getContainerBuilder(): ContainerBuilder { if ($this->containerBuilder) { return $this->containerBuilder; @@ -74,10 +72,9 @@ protected function getContainerBuilder() $kernel = $this->getApplication()->getKernel(); if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { - $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, get_class($kernel)); + $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); $container = $buildContainer(); - $container->getCompilerPassConfig()->setRemovingPasses(array()); - $container->compile(); + $container->getCompilerPassConfig()->setRemovingPasses([]); } else { (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 7da5b6b534b64..d1771da846565 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 4.4.0 ----- - * added `CheckTypeHintsPass` to check injected parameters type during compilation + * added `CheckTypeDeclarationsPass` to check injected parameters type during compilation * added support for opcache.preload by generating a preloading script in the cache folder * added support for dumping the container in one file instead of many files * deprecated support for short factories and short configurators in Yaml diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php new file mode 100644 index 0000000000000..0dfd9ae6f6ca6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -0,0 +1,172 @@ + + * + * 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\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException; +use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * Checks whether injected parameters are compatible with type declarations. + * + * This pass should be run after all optimization passes. + * + * It can be added either: + * * before removing passes to check all services even if they are not currently used, + * * after removing passes to check only services are used in the app. + * + * @author Nicolas Grekas + * @author Julien Maulny + */ +final class CheckTypeDeclarationsPass extends AbstractRecursivePass +{ + private const SCALAR_TYPES = ['int', 'float', 'bool', 'string']; + + private $autoload; + + /** + * @param bool $autoload Whether services who's class in not loaded should be checked or not. + * Defaults to false to save loading code during compilation. + */ + public function __construct(bool $autoload = false) + { + $this->autoload = $autoload; + } + + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + + if (!$this->autoload && !class_exists($class = $value->getClass(), false) && !interface_exists($class, false)) { + return parent::processValue($value, $isRoot); + } + + if (ServiceLocator::class === $value->getClass()) { + return parent::processValue($value, $isRoot); + } + + if ($constructor = $this->getConstructor($value, false)) { + $this->checkTypeDeclarations($value, $constructor, $value->getArguments()); + } + + foreach ($value->getMethodCalls() as $methodCall) { + $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]); + + $this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]); + } + + return parent::processValue($value, $isRoot); + } + + /** + * @throws InvalidArgumentException When not enough parameters are defined for the method + */ + private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void + { + $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters(); + + if (\count($configurationArguments) < $numberOfRequiredParameters) { + throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($configurationArguments))); + } + + $reflectionParameters = $reflectionFunction->getParameters(); + $checksCount = min($reflectionFunction->getNumberOfParameters(), \count($configurationArguments)); + + for ($i = 0; $i < $checksCount; ++$i) { + if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) { + continue; + } + + $this->checkType($checkedDefinition, $configurationArguments[$i], $reflectionParameters[$i]); + } + + if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) { + $variadicParameters = \array_slice($configurationArguments, $lastParameter->getPosition()); + + foreach ($variadicParameters as $variadicParameter) { + $this->checkType($checkedDefinition, $variadicParameter, $lastParameter); + } + } + } + + /** + * @throws InvalidParameterTypeException When a parameter is not compatible with the declared type + */ + private function checkType(Definition $checkedDefinition, $configurationArgument, \ReflectionParameter $parameter): void + { + $parameterTypeName = $parameter->getType()->getName(); + + $referencedDefinition = $configurationArgument; + + if ($referencedDefinition instanceof Reference) { + if (!$this->container->has($referencedDefinition)) { + return; + } + + $referencedDefinition = $this->container->findDefinition((string) $referencedDefinition); + } + + if ('self' === $parameterTypeName) { + $parameterTypeName = $parameter->getDeclaringClass()->getName(); + } + if ('static' === $parameterTypeName) { + $parameterTypeName = $checkedDefinition->getClass(); + } + + if ($referencedDefinition instanceof Definition) { + $class = $referencedDefinition->getClass(); + + if (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) { + return; + } + + if (!is_a($class, $parameterTypeName, true)) { + throw new InvalidParameterTypeException($this->currentId, $class, $parameter); + } + } else { + if (null === $configurationArgument && $parameter->allowsNull()) { + return; + } + + if (\in_array($parameterTypeName, self::SCALAR_TYPES, true) && is_scalar($configurationArgument)) { + return; + } + + if ('iterable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) { + return; + } + + if ('Traversable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) { + return; + } + + if ($configurationArgument instanceof Parameter) { + return; + } + + $checkFunction = sprintf('is_%s', $parameter->getType()->getName()); + + if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) { + throw new InvalidParameterTypeException($this->currentId, \gettype($configurationArgument), $parameter); + } + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php deleted file mode 100644 index 41085b8e68848..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeHintsPass.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * 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\Reference; -use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeHintException; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; - -/** - * Checks whether injected parameters types are compatible with type hints. - * This pass should be run after all optimization passes. - * So it can be added either: - * * before removing (PassConfig::TYPE_BEFORE_REMOVING) so that it will check - * all services, even if they are not currently used, - * * after removing (PassConfig::TYPE_AFTER_REMOVING) so that it will check - * only services you are using. - * - * @author Nicolas Grekas - * @author Julien Maulny - */ -class CheckTypeHintsPass extends AbstractRecursivePass -{ - /** - * If set to true, allows to autoload classes during compilation - * in order to check type hints on parameters that are not yet loaded. - * Defaults to false to prevent code loading during compilation. - * - * @param bool - */ - private $autoload; - - public function __construct(bool $autoload = false) - { - $this->autoload = $autoload; - } - - /** - * {@inheritdoc} - */ - protected function processValue($value, $isRoot = false) - { - if (!$value instanceof Definition) { - return parent::processValue($value, $isRoot); - } - - if (!$this->autoload && !class_exists($className = $this->getClassName($value), false) && !interface_exists($className, false)) { - return parent::processValue($value, $isRoot); - } - - if (ServiceLocator::class === $value->getClass()) { - return parent::processValue($value, $isRoot); - } - - if (null !== $constructor = $this->getConstructor($value, false)) { - $this->checkArgumentsTypeHints($constructor, $value->getArguments()); - } - - foreach ($value->getMethodCalls() as $methodCall) { - $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]); - - $this->checkArgumentsTypeHints($reflectionMethod, $methodCall[1]); - } - - return parent::processValue($value, $isRoot); - } - - /** - * Check type hints for every parameter of a method/constructor. - * - * @throws InvalidArgumentException on type hint incompatibility - */ - private function checkArgumentsTypeHints(\ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void - { - $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters(); - - if (count($configurationArguments) < $numberOfRequiredParameters) { - throw new InvalidArgumentException(sprintf( - 'Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, count($configurationArguments))); - } - - $reflectionParameters = $reflectionFunction->getParameters(); - $checksCount = min($reflectionFunction->getNumberOfParameters(), count($configurationArguments)); - - for ($i = 0; $i < $checksCount; ++$i) { - if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) { - continue; - } - - $this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]); - } - - if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) { - $variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition()); - - foreach ($variadicParameters as $variadicParameter) { - $this->checkTypeHint($variadicParameter, $lastParameter); - } - } - } - - /** - * Check type hints compatibility between - * a definition argument and a reflection parameter. - * - * @throws InvalidArgumentException on type hint incompatibility - */ - private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter): void - { - $referencedDefinition = $configurationArgument; - - if ($referencedDefinition instanceof Reference) { - $referencedDefinition = $this->container->findDefinition((string) $referencedDefinition); - } - - if ($referencedDefinition instanceof Definition) { - $class = $this->getClassName($referencedDefinition); - - if (!$this->autoload && !class_exists($class, false)) { - return; - } - - if (!is_a($class, $parameter->getType()->getName(), true)) { - throw new InvalidParameterTypeHintException($this->currentId, null === $class ? 'null' : $class, $parameter); - } - } else { - if (null === $configurationArgument && $parameter->allowsNull()) { - return; - } - - if ($parameter->getType()->isBuiltin() && is_scalar($configurationArgument)) { - return; - } - - if ('iterable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) { - return; - } - - if ('Traversable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) { - return; - } - - $checkFunction = 'is_'.$parameter->getType()->getName(); - - if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) { - throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter); - } - } - } - - /** - * Get class name from value that can have a factory. - * - * @return string|null - */ - private function getClassName($value) - { - if (is_array($factory = $value->getFactory())) { - list($class, $method) = $factory; - if ($class instanceof Reference) { - $class = $this->container->findDefinition((string) $class)->getClass(); - } elseif (null === $class) { - $class = $value->getClass(); - } elseif ($class instanceof Definition) { - $class = $this->getClassName($class); - } - } else { - $class = $value->getClass(); - } - - return $class; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php new file mode 100644 index 0000000000000..206561fa95a8a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * Thrown when trying to inject a parameter into a constructor/method with an incompatible type. + * + * @author Nicolas Grekas + * @author Julien Maulny + */ +class InvalidParameterTypeException extends InvalidArgumentException +{ + public function __construct(string $serviceId, string $type, \ReflectionParameter $parameter) + { + parent::__construct(sprintf('Invalid definition for service "%s": argument %d of "%s::%s" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $type)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php deleted file mode 100644 index a04d0719729bc..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeHintException.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Exception; - -/** - * Thrown when trying to inject a parameter into a constructor/method - * with a type that does not match type hint. - * - * @author Nicolas Grekas - * @author Julien Maulny - */ -class InvalidParameterTypeHintException extends InvalidArgumentException -{ - public function __construct(string $serviceId, string $typeHint, \ReflectionParameter $parameter) - { - parent::__construct(sprintf( - 'Invalid definition for service "%s": argument %d of "%s::%s" requires a "%s", "%s" passed.', $serviceId, $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $typeHint)); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php similarity index 50% rename from src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php rename to src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 690c628cb64a9..51bc7c6779d20 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeHintsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -12,77 +12,73 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; -use Symfony\Component\DependencyInjection\Compiler\CheckTypeHintsPass; -use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar; -use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgument; -use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgumentNotNull; -use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall; -use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Foo; /** * @author Nicolas Grekas * @author Julien Maulny */ -class CheckTypeHintsPassTest extends TestCase +class CheckTypeDeclarationsPassTest extends TestCase { - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed - */ - public function testProcessThrowsExceptionOnInvalidTypeHintsConstructorArguments() + public function testProcessThrowsExceptionOnInvalidTypesConstructorArguments() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.'); + $container = new ContainerBuilder(); $container->register('foo', Foo::class); $container->register('bar', Bar::class) ->addArgument(new Reference('foo')); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed - */ - public function testProcessThrowsExceptionOnInvalidTypeHintsMethodCallArguments() + public function testProcessThrowsExceptionOnInvalidTypesMethodCallArguments() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.'); + $container = new ContainerBuilder(); $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoo', array(new Reference('foo'))); + ->addMethodCall('setFoo', [new Reference('foo')]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "NULL" passed - */ public function testProcessFailsWhenPassingNullToRequiredArgument() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "NULL" passed.'); + $container = new ContainerBuilder(); $container->register('bar', Bar::class) ->addArgument(null); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct()" requires 1 arguments, 0 passed - */ public function testProcessThrowsExceptionWhenMissingArgumentsInConstructor() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct()" requires 1 arguments, 0 passed.'); + $container = new ContainerBuilder(); $container->register('bar', Bar::class); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } public function testProcessSuccessWhenPassingTooManyArgumentInConstructor() @@ -94,7 +90,7 @@ public function testProcessSuccessWhenPassingTooManyArgumentInConstructor() ->addArgument(new Reference('foo')) ->addArgument(new Reference('foo')); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->addToAssertionCount(1); } @@ -105,62 +101,59 @@ public function testProcessRegisterWithClassName() $container->register(Foo::class, Foo::class); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(Foo::class, $container->get(Foo::class)); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo()" requires 1 arguments, 0 passed - */ public function testProcessThrowsExceptionWhenMissingArgumentsInMethodCall() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo()" requires 1 arguments, 0 passed.'); + $container = new ContainerBuilder(); $container->register('foo', \stdClass::class); $container->register('bar', BarMethodCall::class) ->addArgument(new Reference('foo')) - ->addMethodCall('setFoo', array()); + ->addMethodCall('setFoo', []); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosVariadic" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed - */ public function testProcessVariadicFails() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.'); + $container = new ContainerBuilder(); $container->register('stdClass', \stdClass::class); $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosVariadic', array( + ->addMethodCall('setFoosVariadic', [ new Reference('foo'), new Reference('foo'), new Reference('stdClass'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosVariadic" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed - */ public function testProcessVariadicFailsOnPassingBadTypeOnAnotherArgument() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosVariadic" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.'); + $container = new ContainerBuilder(); $container->register('stdClass', \stdClass::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosVariadic', array( + ->addMethodCall('setFoosVariadic', [ new Reference('stdClass'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } public function testProcessVariadicSuccess() @@ -169,13 +162,13 @@ public function testProcessVariadicSuccess() $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosVariadic', array( + ->addMethodCall('setFoosVariadic', [ new Reference('foo'), new Reference('foo'), new Reference('foo'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); } @@ -186,11 +179,11 @@ public function testProcessSuccessWhenNotUsingOptionalArgument() $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosOptional', array( + ->addMethodCall('setFoosOptional', [ new Reference('foo'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); } @@ -201,33 +194,32 @@ public function testProcessSuccessWhenUsingOptionalArgumentWithGoodType() $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosOptional', array( + ->addMethodCall('setFoosOptional', [ new Reference('foo'), new Reference('foo'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoosOptional" requires a "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo", "stdClass" passed - */ public function testProcessFailsWhenUsingOptionalArgumentWithBadType() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoosOptional" accepts "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo", "stdClass" passed.'); + $container = new ContainerBuilder(); $container->register('stdClass', \stdClass::class); $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoosOptional', array( + ->addMethodCall('setFoosOptional', [ new Reference('foo'), new Reference('stdClass'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } public function testProcessSuccessWhenPassingNullToOptional() @@ -237,37 +229,35 @@ public function testProcessSuccessWhenPassingNullToOptional() $container->register('bar', BarOptionalArgument::class) ->addArgument(null); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertNull($container->get('bar')->foo); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgumentNotNull::__construct" requires a "int", "NULL" passed - */ public function testProcessSuccessWhenPassingNullToOptionalThatDoesNotAcceptNull() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgumentNotNull::__construct" accepts "int", "NULL" passed.'); + $container = new ContainerBuilder(); $container->register('bar', BarOptionalArgumentNotNull::class) ->addArgument(null); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarOptionalArgument::__construct" requires a "stdClass", "string" passed - */ public function testProcessFailsWhenPassingBadTypeToOptional() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarOptionalArgument::__construct" accepts "stdClass", "string" passed.'); + $container = new ContainerBuilder(); $container->register('bar', BarOptionalArgument::class) ->addArgument('string instead of stdClass'); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertNull($container->get('bar')->foo); } @@ -277,97 +267,88 @@ public function testProcessSuccessScalarType() $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setScalars', array( + ->addMethodCall('setScalars', [ 1, 'string', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Bar::__construct" requires a "stdClass", "integer" passed - */ - public function testProcessFailsOnPassingScalarTypeToConstructorTypeHintedWithClass() + public function testProcessFailsOnPassingScalarTypeToConstructorTypedWithClass() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Bar::__construct" accepts "stdClass", "integer" passed.'); + $container = new ContainerBuilder(); $container->register('bar', Bar::class) ->addArgument(1); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setFoo" requires a "stdClass", "string" passed - */ - public function testProcessFailsOnPassingScalarTypeToMethodTypeHintedWithClass() + public function testProcessFailsOnPassingScalarTypeToMethodTypedWithClass() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setFoo" accepts "stdClass", "string" passed.'); + $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setFoo', array( + ->addMethodCall('setFoo', [ 'builtin type instead of class', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\BarMethodCall::setScalars" requires a "int", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed - */ - public function testProcessFailsOnPassingClassToScalarTypeHintedParameter() + public function testProcessFailsOnPassingClassToScalarTypedParameter() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setScalars" accepts "int", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.'); + $container = new ContainerBuilder(); $container->register('foo', Foo::class); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setScalars', array( + ->addMethodCall('setScalars', [ new Reference('foo'), new Reference('foo'), - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * Strict mode not yet handled. - */ public function testProcessSuccessOnPassingBadScalarType() { $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setScalars', array( + ->addMethodCall('setScalars', [ 1, true, - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); } - /** - * Strict mode not yet handled. - */ public function testProcessSuccessPassingBadScalarTypeOptionalArgument() { $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setScalars', array( + ->addMethodCall('setScalars', [ 1, 'string', 'string instead of optional boolean', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); } @@ -377,27 +358,24 @@ public function testProcessSuccessWhenPassingArray() $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setArray', array( - array(), - )); + ->addMethodCall('setArray', [[]]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(BarMethodCall::class, $container->get('bar')); } - public function testProcessSuccessWhenPassingIntegerToArrayTypeHintedParameter() + public function testProcessSuccessWhenPassingIntegerToArrayTypedParameter() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "integer" passed.'); + $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setArray', array( - 1, - )); - - (new CheckTypeHintsPass(true))->process($container); + ->addMethodCall('setArray', [1]); - $this->addToAssertionCount(1); + (new CheckTypeDeclarationsPass(true))->process($container); } public function testProcessSuccessWhenPassingAnIteratorArgumentToIterable() @@ -405,11 +383,9 @@ public function testProcessSuccessWhenPassingAnIteratorArgumentToIterable() $container = new ContainerBuilder(); $container->register('bar', BarMethodCall::class) - ->addMethodCall('setIterable', array( - new IteratorArgument(array()), - )); + ->addMethodCall('setIterable', [new IteratorArgument([])]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->addToAssertionCount(1); } @@ -420,57 +396,43 @@ public function testProcessFactory() $container->register('foo', Foo::class); $container->register('bar', Bar::class) - ->setFactory(array( + ->setFactory([ new Reference('foo'), 'createBar', - )); - - (new CheckTypeHintsPass(true))->process($container); + ]); - $this->assertInstanceOf(Bar::class, $container->get('bar')); - } - - public function testProcessFactoryWhithClassName() - { - $container = new ContainerBuilder(); + /* Asserts that the class of Bar is well detected */ + $container->register('bar_call', BarMethodCall::class) + ->addMethodCall('setBar', [new Reference('bar')]); - $container->register(Foo::class, Foo::class); - $container->register(Bar::class, Bar::class) - ->setFactory(array( - new Reference(Foo::class), - 'createBar', - )); - - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); - $this->assertInstanceOf(Bar::class, $container->get(Bar::class)); + $this->assertInstanceOf(Bar::class, $container->get('bar')); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 0 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo::createBarArguments" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed - */ public function testProcessFactoryFailsOnInvalidParameterType() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.'); + $container = new ContainerBuilder(); $container->register('foo', Foo::class); $container->register('bar', Bar::class) ->addArgument(new Reference('foo')) - ->setFactory(array( + ->setFactory([ new Reference('foo'), 'createBarArguments', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Invalid definition for service "bar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo::createBarArguments" requires a "stdClass", "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeHintsPass\Foo" passed - */ public function testProcessFactoryFailsOnInvalidParameterTypeOptional() { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar": argument 2 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo::createBarArguments" accepts "stdClass", "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\Foo" passed.'); + $container = new ContainerBuilder(); $container->register('stdClass', \stdClass::class); @@ -478,12 +440,12 @@ public function testProcessFactoryFailsOnInvalidParameterTypeOptional() $container->register('bar', Bar::class) ->addArgument(new Reference('stdClass')) ->addArgument(new Reference('foo')) - ->setFactory(array( + ->setFactory([ new Reference('foo'), 'createBarArguments', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); } public function testProcessFactorySuccessOnValidTypes() @@ -495,12 +457,12 @@ public function testProcessFactorySuccessOnValidTypes() $container->register('bar', Bar::class) ->addArgument(new Reference('stdClass')) ->addArgument(new Reference('stdClass')) - ->setFactory(array( + ->setFactory([ new Reference('foo'), 'createBarArguments', - )); + ]); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->addToAssertionCount(1); } @@ -512,7 +474,7 @@ public function testProcessFactoryCallbackSuccessOnValidType() $container->register('bar', \DateTime::class) ->setFactory('date_create'); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(\DateTime::class, $container->get('bar')); } @@ -524,13 +486,13 @@ public function testProcessDoesNotLoadCodeByDefault() $container->register('foo', FooNotExisting::class); $container->register('bar', BarNotExisting::class) ->addArgument(new Reference('foo')) - ->addMethodCall('setFoo', array( + ->addMethodCall('setFoo', [ new Reference('foo'), 'string', 1, - )); + ]); - (new CheckTypeHintsPass())->process($container); + (new CheckTypeDeclarationsPass())->process($container); $this->addToAssertionCount(1); } @@ -541,12 +503,12 @@ public function testProcessFactoryDoesNotLoadCodeByDefault() $container->register('foo', FooNotExisting::class); $container->register('bar', BarNotExisting::class) - ->setFactory(array( + ->setFactory([ new Reference('foo'), 'notExistingMethod', - )); + ]); - (new CheckTypeHintsPass())->process($container); + (new CheckTypeDeclarationsPass())->process($container); $this->addToAssertionCount(1); } @@ -558,12 +520,12 @@ public function testProcessPassingBuiltinTypeDoesNotLoadCodeByDefault() $container->register('bar', BarNotExisting::class) ->addArgument(1); - (new CheckTypeHintsPass())->process($container); + (new CheckTypeDeclarationsPass())->process($container); $this->addToAssertionCount(1); } - public function testProcessDoesNotThrowsExceptionOnValidTypeHints() + public function testProcessDoesNotThrowsExceptionOnValidTypes() { $container = new ContainerBuilder(); @@ -571,7 +533,22 @@ public function testProcessDoesNotThrowsExceptionOnValidTypeHints() $container->register('bar', Bar::class) ->addArgument(new Reference('foo')); - (new CheckTypeHintsPass(true))->process($container); + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo); + } + + public function testProcessThrowsOnIterableTypeWhenScalarPassed() + { + $this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid definition for service "bar_call": argument 1 of "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CheckTypeDeclarationsPass\\BarMethodCall::setIterable" accepts "iterable", "integer" passed.'); + + $container = new ContainerBuilder(); + + $container->register('bar_call', BarMethodCall::class) + ->addMethodCall('setIterable', [2]); + + (new CheckTypeDeclarationsPass(true))->process($container); $this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Bar.php similarity index 88% rename from src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php rename to src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Bar.php index 85a1289815f85..403841ce88df3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Bar.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/Bar.php @@ -1,6 +1,6 @@ foo = $foo; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php similarity index 89% rename from src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php rename to src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php index 3b6daa77f84dd..4f348895132ca 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/BarOptionalArgument.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarOptionalArgument.php @@ -1,6 +1,6 @@ Date: Sat, 12 Oct 2019 14:06:59 +0200 Subject: [PATCH 3/3] Make it really work on real apps --- .../Bundle/FrameworkBundle/CHANGELOG.md | 2 +- .../Command/ContainerLintCommand.php | 10 +-- .../Resources/config/validator.xml | 2 +- .../Bundle/TestBundle/TestBundle.php | 12 +++ .../Tests/Functional/Bundle/TestBundle.php | 38 ++++++++ .../app/AbstractTokenCompareRoles/bundles.php | 2 + .../Functional/app/AliasedEvents/bundles.php | 2 + .../app/AutowiringTypes/bundles.php | 1 + .../Functional/app/CsrfFormLogin/bundles.php | 1 + .../app/FirewallEntryPoint/bundles.php | 1 + .../Functional/app/JsonLogin/bundles.php | 1 + .../Functional/app/JsonLoginLdap/bundles.php | 1 + .../Functional/app/LogoutAccess/bundles.php | 2 + .../bundles.php | 2 + .../app/MissingUserProvider/bundles.php | 2 + .../Functional/app/PasswordEncode/bundles.php | 1 + .../app/RememberMeLogout/bundles.php | 2 + .../Functional/app/SecurityHelper/bundles.php | 2 + .../app/StandardFormLogin/bundles.php | 2 + .../Bundle/SecurityBundle/composer.json | 2 +- .../Compiler/AbstractRecursivePass.php | 3 + .../Compiler/CheckTypeDeclarationsPass.php | 90 +++++++++++-------- 22 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index b8508809ae9cd..2e4ae15a4e9da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 4.4.0 ----- - * Added `lint:container` to check that services wiring matches type declarations + * Added `lint:container` command to check that services wiring matches type declarations * Added `MailerAssertionsTrait` * Deprecated support for `templating` engine in `TemplateController`, use Twig instead * Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index 04ecff767f5f3..c5cba6c9a5286 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -15,7 +15,6 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -39,7 +38,6 @@ protected function configure() $this ->setDescription('Ensures that arguments injected into services match type declarations') ->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.') - ->addOption('ignore-unused-services', 'o', InputOption::VALUE_NONE, 'Ignore unused services') ; } @@ -50,13 +48,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $container = $this->getContainerBuilder(); + $container->setParameter('container.build_hash', 'lint_container'); + $container->setParameter('container.build_time', time()); $container->setParameter('container.build_id', 'lint_container'); - $container->addCompilerPass( - new CheckTypeDeclarationsPass(true), - $input->getOption('ignore-unused-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_OPTIMIZE, - -5 - ); + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); $container->compile(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index f3f73a9c2a6b5..f2d0e9e2a603b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -17,7 +17,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php index d90041213ce31..ab0b06ffb3707 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -14,6 +14,8 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\AnnotationReaderPass; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\TranslationDebugPass; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -31,5 +33,15 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new TranslationDebugPass()); + + $container->addCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container) + { + $container->removeDefinition('twig.controller.exception'); + $container->removeDefinition('twig.controller.preview_error'); + } + }); + + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php new file mode 100644 index 0000000000000..5197a16195e2e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle; + +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class TestBundle extends Bundle +{ + public function build(ContainerBuilder $container) + { + $container->setParameter('container.build_hash', 'test_bundle'); + $container->setParameter('container.build_time', time()); + $container->setParameter('container.build_id', 'test_bundle'); + + $container->addCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container) + { + $container->removeDefinition('twig.controller.exception'); + $container->removeDefinition('twig.controller.preview_error'); + } + }); + + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php index bedfbb1bd82a9..054405274e83b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php @@ -12,9 +12,11 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\SecuredPageBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), new SecuredPageBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php index d7b7c498f84a0..115dd2c357e86 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php @@ -12,9 +12,11 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), new EventBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php index 535a4bf517b80..794461855cb8d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php @@ -13,4 +13,5 @@ new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiringBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php index 65a38200e759c..81f9c48b64ca8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php @@ -14,4 +14,5 @@ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\CsrfFormLoginBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php index 7928a468da7f6..b77f03be2703b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php @@ -13,4 +13,5 @@ new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\FirewallEntryPointBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php index cd367a95b477b..bbb9107456b98 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php @@ -13,4 +13,5 @@ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php index bcfd17425cfd1..edf6dae14c064 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php @@ -12,4 +12,5 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php index ccff0d356cab9..0e34621a35ccd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php @@ -12,9 +12,11 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\MissingUserProviderBundle\MissingUserProviderBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), new MissingUserProviderBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php index bcfd17425cfd1..edf6dae14c064 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php @@ -12,4 +12,5 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php index 95041e7ad465e..cef48bfcc4b46 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php @@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; use Symfony\Bundle\TwigBundle\TwigBundle; return [ @@ -19,4 +20,5 @@ new SecurityBundle(), new TwigBundle(), new FormLoginBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 77bd4a0cfb76f..7ecd3a12c13ec 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -19,7 +19,7 @@ "php": "^7.1.3", "ext-xml": "*", "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^4.2|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", "symfony/http-kernel": "^4.4", "symfony/security-core": "^4.4", "symfony/security-csrf": "^4.2|^5.0", diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 8453e4e62ada7..ad3cb5295cc7e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -133,9 +133,12 @@ protected function getConstructor(Definition $definition, $required) list($class, $method) = $factory; if ($class instanceof Reference) { $class = $this->container->findDefinition((string) $class)->getClass(); + } elseif ($class instanceof Definition) { + $class = $class->getClass(); } elseif (null === $class) { $class = $definition->getClass(); } + if ('__construct' === $method) { throw new RuntimeException(sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId)); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index 0dfd9ae6f6ca6..9bc57539f412b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException; @@ -79,27 +80,27 @@ protected function processValue($value, $isRoot = false) /** * @throws InvalidArgumentException When not enough parameters are defined for the method */ - private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void + private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void { $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters(); - if (\count($configurationArguments) < $numberOfRequiredParameters) { - throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($configurationArguments))); + if (\count($values) < $numberOfRequiredParameters) { + throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values))); } $reflectionParameters = $reflectionFunction->getParameters(); - $checksCount = min($reflectionFunction->getNumberOfParameters(), \count($configurationArguments)); + $checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values)); for ($i = 0; $i < $checksCount; ++$i) { if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) { continue; } - $this->checkType($checkedDefinition, $configurationArguments[$i], $reflectionParameters[$i]); + $this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]); } if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) { - $variadicParameters = \array_slice($configurationArguments, $lastParameter->getPosition()); + $variadicParameters = \array_slice($values, $lastParameter->getPosition()); foreach ($variadicParameters as $variadicParameter) { $this->checkType($checkedDefinition, $variadicParameter, $lastParameter); @@ -110,63 +111,82 @@ private function checkTypeDeclarations(Definition $checkedDefinition, \Reflectio /** * @throws InvalidParameterTypeException When a parameter is not compatible with the declared type */ - private function checkType(Definition $checkedDefinition, $configurationArgument, \ReflectionParameter $parameter): void + private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void { - $parameterTypeName = $parameter->getType()->getName(); + $type = $parameter->getType()->getName(); - $referencedDefinition = $configurationArgument; + if ($value instanceof Reference) { + if (!$this->container->has($value = (string) $value)) { + return; + } - if ($referencedDefinition instanceof Reference) { - if (!$this->container->has($referencedDefinition)) { + if ('service_container' === $value && is_a($type, Container::class, true)) { return; } - $referencedDefinition = $this->container->findDefinition((string) $referencedDefinition); + $value = $this->container->findDefinition($value); } - if ('self' === $parameterTypeName) { - $parameterTypeName = $parameter->getDeclaringClass()->getName(); + if ('self' === $type) { + $type = $parameter->getDeclaringClass()->getName(); } - if ('static' === $parameterTypeName) { - $parameterTypeName = $checkedDefinition->getClass(); + + if ('static' === $type) { + $type = $checkedDefinition->getClass(); } - if ($referencedDefinition instanceof Definition) { - $class = $referencedDefinition->getClass(); + if ($value instanceof Definition) { + $class = $value->getClass(); if (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) { return; } - if (!is_a($class, $parameterTypeName, true)) { - throw new InvalidParameterTypeException($this->currentId, $class, $parameter); - } - } else { - if (null === $configurationArgument && $parameter->allowsNull()) { + if ('callable' === $type && method_exists($class, '__invoke')) { return; } - if (\in_array($parameterTypeName, self::SCALAR_TYPES, true) && is_scalar($configurationArgument)) { + if ('iterable' === $type && is_subclass_of($class, 'Traversable')) { return; } - if ('iterable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) { + if (is_a($class, $type, true)) { return; } - if ('Traversable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) { - return; - } + throw new InvalidParameterTypeException($this->currentId, $class, $parameter); + } - if ($configurationArgument instanceof Parameter) { - return; - } + if ($value instanceof Parameter) { + $value = $this->container->getParameter($value); + } elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) { + $value = $this->container->getParameter($match[1]); + } - $checkFunction = sprintf('is_%s', $parameter->getType()->getName()); + if (null === $value && $parameter->allowsNull()) { + return; + } - if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) { - throw new InvalidParameterTypeException($this->currentId, \gettype($configurationArgument), $parameter); - } + if (\in_array($type, self::SCALAR_TYPES, true) && is_scalar($value)) { + return; + } + + if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition)) { + return; + } + + if ('iterable' === $type && (\is_array($value) || $value instanceof \Traversable || $value instanceof IteratorArgument)) { + return; + } + + if ('Traversable' === $type && ($value instanceof \Traversable || $value instanceof IteratorArgument)) { + return; + } + + $checkFunction = sprintf('is_%s', $parameter->getType()->getName()); + + if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) { + throw new InvalidParameterTypeException($this->currentId, \gettype($value), $parameter); } } } 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