diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php new file mode 100644 index 0000000000..c7c6cb4507 --- /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 f13aa759d3..eff7ec6694 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 c90cfa7471..7da5b6b534 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 0000000000..41085b8e68 --- /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 0000000000..a04d071972 --- /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 0000000000..690c628cb6 --- /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 0000000000..85a1289815 --- /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 0000000000..076c219d3a --- /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 0000000000..3b6daa77f8 --- /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 0000000000..5e54114aef --- /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 0000000000..9132d6a246 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeHintsPass/Foo.php @@ -0,0 +1,16 @@ +