Skip to content

Commit cc60abc

Browse files
alcalynnicolas-grekas
authored andcommitted
[DI] Add compiler pass to check arguments type hint
1 parent 820e4e2 commit cc60abc

File tree

9 files changed

+881
-0
lines changed

9 files changed

+881
-0
lines changed

CHANGELOG.md

Lines 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

Compiler/CheckTypeHintsPass.php

Lines 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 <[email protected]>
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 <[email protected]>
31+
* @author Julien Maulny <[email protected]>
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+
}
Lines 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 <[email protected]>
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 <[email protected]>
19+
* @author Julien Maulny <[email protected]>
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)