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

Skip to content

Commit 9ff1945

Browse files
committed
[symfony#27744] Add compiler pass to check arguments type hint
1 parent 83232f8 commit 9ff1945

File tree

6 files changed

+602
-0
lines changed

6 files changed

+602
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
class CheckTypeHintsPass extends AbstractRecursivePass
20+
{
21+
private $autoload;
22+
23+
public function __construct(bool $autoload = false)
24+
{
25+
$this->autoload = $autoload;
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
protected function processValue($value, $isRoot = false)
32+
{
33+
if (!$value instanceof Definition) {
34+
return parent::processValue($value, $isRoot);
35+
}
36+
37+
// When service is declared with $container->register(Foo::class); and not $container->register(Foo::class, Foo::class);
38+
if (null === $value->getClass()) {
39+
return parent::processValue($value, $isRoot);
40+
}
41+
42+
if (!$this->autoload && !\class_exists($this->getClassName($value), false)) {
43+
return parent::processValue($value, $isRoot);
44+
}
45+
46+
if (null !== $constructor = $this->getConstructor($value, false)) {
47+
$this->checkArgumentsTypeHints($constructor, $value->getArguments());
48+
}
49+
50+
foreach ($value->getMethodCalls() as $methodCall) {
51+
$reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
52+
53+
$this->checkArgumentsTypeHints($reflectionMethod /* warn */, $methodCall[1]);
54+
}
55+
56+
return parent::processValue($value, $isRoot);
57+
}
58+
59+
/**
60+
* Check type hints for every parameter of a method/constructor.
61+
*
62+
* @param \ReflectionMethod $reflectionMethod
63+
* @param Reference[] $configurationArguments
64+
*/
65+
private function checkArgumentsTypeHints(\ReflectionMethod $reflectionMethod, array $configurationArguments)
66+
{
67+
$numberOfRequiredParameters = $reflectionMethod->getNumberOfRequiredParameters();
68+
69+
if (count($configurationArguments) < $numberOfRequiredParameters) {
70+
throw new InvalidArgumentException(sprintf(
71+
'In service declaration "%s". Missing parameter in "%s::%s". Expecting %d required parameters, got only %d.',
72+
$this->currentId,
73+
$reflectionMethod->getDeclaringClass()->getName(),
74+
$reflectionMethod->getName(),
75+
$numberOfRequiredParameters,
76+
count($configurationArguments)
77+
));
78+
}
79+
80+
$reflectionParameters = $reflectionMethod->getParameters();
81+
$checksCount = min($reflectionMethod->getNumberOfParameters(), count($configurationArguments));
82+
83+
if ($reflectionMethod->isVariadic()) {
84+
$checksCount--;
85+
}
86+
87+
for ($i = 0; $i < $checksCount; $i++) {
88+
if (!$reflectionParameters[$i]->hasType()) {
89+
continue;
90+
}
91+
92+
$this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]);
93+
}
94+
95+
if ($reflectionMethod->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
96+
$variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition());
97+
98+
foreach ($variadicParameters as $variadicParameter) {
99+
$this->checkTypeHint($variadicParameter, $lastParameter);
100+
}
101+
}
102+
}
103+
104+
/**
105+
* Check type hints compatibility between
106+
* a configuration argument and a reflection parameter.
107+
*
108+
* @param Reference|mixed $configurationArgument Reference of a service or scalar.
109+
* @param \ReflectionParameter $parameter The PHP type hinted parameter to compare to.
110+
*
111+
* @throws InvalidArgumentException On type hint incompatibility.
112+
*/
113+
private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter)
114+
{
115+
if ($configurationArgument instanceof Reference) {
116+
$argumentDefinition = $this->container->findDefinition((string) $configurationArgument);
117+
$class = $this->getClassName($argumentDefinition);
118+
119+
if (!$this->autoload && !\class_exists($class, false)) {
120+
return;
121+
}
122+
123+
if (!\is_a($class, $parameter->getType()->getName(), true)) {
124+
throw new InvalidParameterTypeHintException($this->currentId, $class, $parameter);
125+
}
126+
} else {
127+
$checkFunction = 'is_'.$parameter->getType()->getName();
128+
129+
if (!$checkFunction($configurationArgument)) {
130+
throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter);
131+
}
132+
}
133+
}
134+
135+
/**
136+
* Get class name from value that can have a factory.
137+
*
138+
* @param mixed $value
139+
*
140+
* @return string
141+
*/
142+
private function getClassName($value)
143+
{
144+
if (is_array($factory = $value->getFactory())) {
145+
list($class, $method) = $factory;
146+
10000 if ($class instanceof Reference) {
147+
$class = $this->container->findDefinition((string) $class)->getClass();
148+
} elseif (null === $class) {
149+
$class = $value->getClass();
150+
} elseif ($class instanceof Definition) {
151+
$class = $class->getClass();
152+
}
153+
} else {
154+
$class = is_string($factory) ? null : $value->getClass();
155+
}
156+
157+
return $class;
158+
}
159+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
class InvalidParameterTypeHintException extends InvalidArgumentException
19+
{
20+
public function __construct(string $serviceId, string $typeHint, \ReflectionParameter $parameter)
21+
{
22+
parent::__construct(sprintf(
23+
'In service declaration "%s". Trying to inject a "%s" as argument %d of "%s::%s", but the type hint expects a "%s".',
24+
$serviceId,
25+
$typeHint,
26+
$parameter->getPosition(),
27+
$parameter->getDeclaringClass()->getName(),
28+
$parameter->getDeclaringFunction()->getName(),
29+
$parameter->getType()->getName()
30+
));
31+
}
32+
}

0 commit comments

Comments
 (0)
0