Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a6292b9

Browse filesBrowse files
alcalynnicolas-grekas
authored andcommitted
[DI] Add compiler pass to check arguments type hint
1 parent 211c651 commit a6292b9
Copy full SHA for a6292b9

File tree

Expand file treeCollapse file tree

11 files changed

+972
-0
lines changed
Filter options
Expand file treeCollapse file tree

11 files changed

+972
-0
lines changed
+87Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Config\ConfigCache;
19+
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
20+
use Symfony\Component\DependencyInjection\ContainerBuilder;
21+
use Symfony\Component\DependencyInjection\Compiler\CheckTypeHintsPass;
22+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
23+
use Symfony\Component\Config\FileLocator;
24+
25+
class ContainerLintCommand extends Command
26+
{
27+
/**
28+
* @var ContainerBuilder
29+
*/
30+
private $containerBuilder;
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
protected function configure()
36+
{
37+
$this
38+
->setDescription('Lints container for services arguments type hints')
39+
->setHelp('This command will parse all your defined services and check that you are injecting service without type error based on type hints.')
40+
->addOption('only-used-services', 'o', InputOption::VALUE_NONE, 'Check only services that are used in your application')
41+
;
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function execute(InputInterface $input, OutputInterface $output)
48+
{
49+
$container = $this->getContainerBuilder();
50+
51+
$container->setParameter('container.build_id', 'lint_container');
52+
53+
$container->addCompilerPass(
54+
new CheckTypeHintsPass(),
55+
$input->getOption('only-used-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_BEFORE_OPTIMIZATION
56+
);
57+
58+
$container->compile();
59+
}
60+
61+
/**
62+
* Loads the ContainerBuilder from the cache.
63+
*
64+
* @return ContainerBuilder
65+
*
66+
* @throws \LogicException
67+
*/
68+
protected function getContainerBuilder()
69+
{
70+
if ($this->containerBuilder) {
71+
return $this->containerBuilder;
72+
}
73+
74+
$kernel = $this->getApplication()->getKernel();
75+
76+
if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) {
77+
$buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, get_class($kernel));
78+
$container = $buildContainer();
79+
$container->getCompilerPassConfig()->setRemovingPasses(array());
80+
$container->compile();
81+
} else {
82+
(new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump'));
83+
}
84+
85+
return $this->containerBuilder = $container;
86+
}
87+
}

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
<tag name="console.command" command="debug:container" />
7171
</service>
7272

73+
<service id="console.command.container_lint" class="Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand">
74+
<tag name="console.command" command="lint:container" />
75+
</service>
76+
7377
<service id="console.command.debug_autowiring" class="Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand">
7478
<argument>null</argument>
7579
<argument type="service" id="debug.file_link_formatter" on-invalid="null"/>

‎src/Symfony/Component/DependencyInjection/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
4.4.0
55
-----
66

7+
* added `CheckTypeHintsPass` to check injected parameters type during compilation
78
* added support for opcache.preload by generating a preloading script in the cache folder
89
* added support for dumping the container in one file instead of many files
910
* deprecated support for short factories and short configurators in Yaml
+184Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
use Symfony\Component\DependencyInjection\ServiceLocator;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeHintException;
19+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
20+
21+
/**
22+
* Checks whether injected parameters types are compatible with type hints.
23+
* This pass should be run after all optimization passes.
24+
* So it can be added either:
25+
* * before removing (PassConfig::TYPE_BEFORE_REMOVING) so that it will check
26+
* all services, even if they are not currently used,
27+
* * after removing (PassConfig::TYPE_AFTER_REMOVING) so that it will check
28+
* only services you are using.
29+
*
30+
* @author Nicolas Grekas <p@tchwork.com>
31+
* @author Julien Maulny <jmaulny@darkmira.fr>
32+
*/
33+
class CheckTypeHintsPass extends AbstractRecursivePass
34+
{
35+
/**
36+
* If set to true, allows to autoload classes during compilation
37+
* in order to check type hints on parameters that are not yet loaded.
38+
* Defaults to false to prevent code loading during compilation.
39+
*
40+
* @param bool
41+
*/
42+
private $autoload;
43+
44+
public function __construct(bool $autoload = false)
45+
{
46+
$this->autoload = $autoload;
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
protected function processValue($value, $isRoot = false)
53+
{
54+
if (!$value instanceof Definition) {
55+
return parent::processValue($value, $isRoot);
56+
}
57+
58+
if (!$this->autoload && !class_exists($className = $this->getClassName($value), false) && !interface_exists($className, false)) {
59+
return parent::processValue($value, $isRoot);
60+
}
61+
62+
if (ServiceLocator::class === $value->getClass()) {
63+
return parent::processValue($value, $isRoot);
64+
}
65+
66+
if (null !== $constructor = $this->getConstructor($value, false)) {
67+
$this->checkArgumentsTypeHints($constructor, $value->getArguments());
68+
}
69+
70+
foreach ($value->getMethodCalls() as $methodCall) {
71+
$reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
72+
73+
$this->checkArgumentsTypeHints($reflectionMethod, $methodCall[1]);
74+
}
75+
76+
return parent::processValue($value, $isRoot);
77+
}
78+
79+
/**
80+
* Check type hints for every parameter of a method/constructor.
81+
*
82+
* @throws InvalidArgumentException on type hint incompatibility
83+
*/
84+
private function checkArgumentsTypeHints(\ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void
85+
{
86+
$numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
87+
88+
if (count($configurationArguments) < $numberOfRequiredParameters) {
89+
throw new InvalidArgumentException(sprintf(
90+
'Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, count($configurationArguments)));
91+
}
92+
93+
$reflectionParameters = $reflectionFunction->getParameters();
94+
$checksCount = min($reflectionFunction->getNumberOfParameters(), count($configurationArguments));
95+
96+
for ($i = 0; $i < $checksCount; ++$i) {
97+
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
98+
continue;
99+
}
100+
101+
$this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]);
102+
}
103+
104+
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
105+
$variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition());
106+
107+
foreach ($variadicParameters as $variadicParameter) {
108+
$this->checkTypeHint($variadicParameter, $lastParameter);
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Check type hints compatibility between
115+
* a definition argument and a reflection parameter.
116+
*
117+
* @throws InvalidArgumentException on type hint incompatibility
118+
*/
119+
private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter): void
120+
{
121+
$referencedDefinition = $configurationArgument;
122+
123+
if ($referencedDefinition instanceof Reference) {
124+
$referencedDefinition = $this->container->findDefinition((string) $referencedDefinition);
125+
}
126+
127+
if ($referencedDefinition instanceof Definition) {
128+
$class = $this->getClassName($referencedDefinition);
129+
130+
if (!$this->autoload && !class_exists($class, false)) {
131+
return;
132+
}
133+
134+
if (!is_a($class, $parameter->getType()->getName(), true)) {
135+
throw new InvalidParameterTypeHintException($this->currentId, null === $class ? 'null' : $class, $parameter);
136+
}
137+
} else {
138+
if (null === $configurationArgument && $parameter->allowsNull()) {
139+
return;
140+
}
141+
142+
if ($parameter->getType()->isBuiltin() && is_scalar($configurationArgument)) {
143+
return;
144+
}
145+
146+
if ('iterable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) {
147+
return;
148+
}
149+
150+
if ('Traversable' === $parameter->getType()->getName() && $configurationArgument instanceof IteratorArgument) {
151+
return;
152+
}
153+
154+
$checkFunction = 'is_'.$parameter->getType()->getName();
155+
156+
if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) {
157+
throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter);
158+
}
159+
}
160+
}
161+
162+
/**
163+
* Get class name from value that can have a factory.
164+
*
165+
* @return string|null
166+
*/
167+
private function getClassName($value)
168+
{
169+
if (is_array($factory = $value->getFactory())) {
170+
list($class, $method) = $factory;
171+
if ($class instanceof Reference) {
172+
$class = $this->container->findDefinition((string) $class)->getClass();
173+
} elseif (null === $class) {
174+
$class = $value->getClass();
175+
} elseif ($class instanceof Definition) {
176+
$class = $this->getClassName($class);
177+
}
178+
} else {
179+
$class = $value->getClass();
180+
}
181+
182+
return $class;
183+
}
184+
}
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Exception;
13+
14+
/**
15+
* Thrown when trying to inject a parameter into a constructor/method
16+
* with a type that does not match type hint.
17+
*
18+
* @author Nicolas Grekas <p@tchwork.com>
19+
* @author Julien Maulny <jmaulny@darkmira.fr>
20+
*/
21+
class InvalidParameterTypeHintException extends InvalidArgumentException
22+
{
23+
public function __construct(string $serviceId, string $typeHint, \ReflectionParameter $parameter)
24+
{
25+
parent::__construct(sprintf(
26+
'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));
27+
}
28+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.