8000 [#27744] Add compiler pass to check arguments type hint · alcalyn/symfony@7a48a03 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7a48a03

Browse files
committed
[symfony#27744] Add compiler pass to check arguments type hint
1 parent 3cfdc9e commit 7a48a03

File tree

9 files changed

+825
-0
lines changed

9 files changed

+825
-0
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* added `ServiceSubscriberTrait`
88
* added `ServiceLocatorArgument` for creating optimized service-locators
9+
* added `CheckTypeHintsPass` to check injected parameters type during compilation
910

1011
4.1.0
1112
-----
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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\Exception\InvalidArgumentException;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeHintException;
18+
19+
/**
20+
* Checks whether injected parameters types are compatible with type hints.
21+
* This pass should be added before removing (PassConfig::TYPE_BEFORE_REMOVING).
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
* @author Julien Maulny <jmaulny@darkmira.fr>
25+
*/
26+
class CheckTypeHintsPass extends AbstractRecursivePass
27+
{
28+
/**
29+
* If set to true, allows to autoload classes during compilation
30+
* in order to check type hints on parameters that are not yet loaded.
31+
* Defaults to false to prevent code loading during compilation.
32+
*
33+
* @param bool
34+
*/
35+
private $autoload;
36+
37+
public function __construct(bool $autoload = false)
38+
{
39+
$this->autoload = $autoload;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
protected function processValue($value, $isRoot = false)
46+
{
47+
if (!$value instanceof Definition) {
48+
return parent::processValue($value, $isRoot);
49+
}
50+
51+
if (!$this->autoload && !class_exists($this->getClassName($value), false)) {
52+
return parent::processValue($value, $isRoot);
53+
}
54+
55+
if (null !== $constructor = $this->getConstructor($value, false)) {
56+
$this->checkArgumentsTypeHints($constructor, $value->getArguments());
57+
}
58+
59+
foreach ($value->getMethodCalls() as $methodCall) {
60+
$reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
61+
62+
$this->checkArgumentsTypeHints($reflectionMethod, $methodCall[1]);
63+
}
64+
65+
return parent::processValue($value, $isRoot);
66+
}
67+
68+
/**
69+
* Check type hints for every parameter of a method/constructor.
70+
*
71+
* @throws InvalidArgumentException on type hint incompatibility
72+
*/
73+
private function checkArgumentsTypeHints(\ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void
74+
{
75+
$numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
76+
77+
if (count($configurationArguments) < $numberOfRequiredParameters) {
78+
throw new InvalidArgumentException(sprintf(
79+
'Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, count($configurationArguments)));
80+
}
81+
82+
$reflectionParameters = $reflectionFunction->getParameters();
83+
$checksCount = min($reflectionFunction->getNumberOfParameters(), count($configurationArguments));
84+
85+
if ($reflectionFunction->isVariadic()) {
86+
--$checksCount;
87+
}
88+
89+
for ($i = 0; $i < $checksCount; ++$i) {
90+
if (!$reflectionParameters[$i]->hasType()) {
91+
continue;
92+
}
93+
94+
$this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]);
95+
}
96+
97+
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
98+
$variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition());
99+
100+
foreach ($variadicParameters as $variadicParameter) {
101+
$this->checkTypeHint($variadicParameter, $lastParameter);
102+
}
103+
}
104+
}
105+
106+
/**
107+
* Check type hints compatibility between
108+
* a definition argument and a reflection parameter.
109+
*
110+
* @throws InvalidArgumentException on type hint incompatibility
111+
*/
112+
private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter): void
113+
{
114+
if ($configurationArgument instanceof Reference) {
115+
$referencedDefinition = $this->container->findDefinition((string) $configurationArgument);
116+
$class = $this->getClassName($referencedDefinition);
117+
118+
if (!$this->autoload && !class_exists($class, false)) {
119+
return;
120+
}
121+
122+
if (!is_a($class, $parameter->getType()->getName(), true)) {
123+
throw new InvalidParameterTypeHintException($this->currentId, null === $class ? 'null' : $class, $parameter);
124+
}
125+
} else {
126+
if (null === $configurationArgument && $parameter->allowsNull()) {
127+
return;
128+
}
129+
130+
if ($parameter->getType()->isBuiltin() && is_scalar($configurationArgument)) {
131+
return;
132+
}
133+
134+
$checkFunction = 'is_'.$parameter->getType()->getName();
135+
136+
if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) {
137+
throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter);
138+
}
139+
}
140+
}
141+
142+
/**
143+
* Get class name from value that can have a factory.
144+
*
145+
* @return string|null
146+
*/
147+
private function getClassName($value)
148+
{
149+
if (is_array($factory = $value->getFactory())) {
150+
list($class, $method) = $factory;
151+
if ($class instanceof Reference) {
152+
$class = $this->container->findDefinition((string) $class)->getClass();
153+
} elseif (null === $class) {
154+
$class = $value->getClass();
155+
} elseif ($class instanceof Definition) {
156+
$class = $class->getClass();
157+
}
158+
} else {
159+
$class = is_string($factory) ? null : $value->getClass();
160+
}
161+
162+
return $class;
163+
}
164+
}
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 <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