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

Skip to content

Commit 4b59201

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

File tree

9 files changed

+843
-0
lines changed

9 files changed

+843
-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: 165 additions & 0 deletions
< F438 /tr>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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($className = $this->getClassName($value), false) && !interface_exists($className, 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 $metho 67E6 dCall) {
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+
for ($i = 0; $i < $checksCount; ++$i) {
86+
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
87+
continue;
88+
}
89+
90+
$this->checkTypeHint($configurationArguments[$i], $reflectionParameters[$i]);
91+
}
92+
93+
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
94+
$variadicParameters = array_slice($configurationArguments, $lastParameter->getPosition());
95+
96+
foreach ($variadicParameters as $variadicParameter) {
97+
$this->checkTypeHint($variadicParameter, $lastParameter);
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Check type hints compatibility between
104+
* a definition argument and a reflection parameter.
105+
*
106+
* @throws InvalidArgumentException on type hint incompatibility
107+
*/
108+
private function checkTypeHint($configurationArgument, \ReflectionParameter $parameter): void
109+
{
110+
$referencedDefinition = $configurationArgument;
111+
112+
if ($referencedDefinition instanceof Reference) {
113+
$referencedDefinition = $this->container->findDefinition((string) $referencedDefinition);
114+
}
115+
116+
if ($referencedDefinition instanceof Definition) {
117+
$class = $this->getClassName($referencedDefinition);
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, null === $class ? 'null' : $class, $parameter);
125+
}
126+
} else {
127+
if (null === $configurationArgument && $parameter->allowsNull()) {
128+
return;
129+
}
130+
131+
if ($parameter->getType()->isBuiltin() && is_scalar($configurationArgument)) {
132+
return;
133+
}
134+
135+
$checkFunction = 'is_'.$parameter->getType()->getName();
136+
137+
if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) {
138+
throw new InvalidParameterTypeHintException($this->currentId, gettype($configurationArgument), $parameter);
139+
}
140+
}
141+
}
142+
143+
/**
144+
* Get class name from value that can have a factory.
145+
*
146+
* @return string|null
147+
*/
148+
private function getClassName($value)
149+
{
150+
if (is_array($factory = $value->getFactory())) {
151+
list($class, $method) = $factory;
152+
if ($class instanceof Reference) {
153+
$class = $this->container->findDefinition((string) $class)->getClass();
154+
} elseif (null === $class) {
155+
$class = $value->getClass();
156+
} elseif ($class instanceof Definition) {
157+
$class = $this->getClassName($class);
158+
}
159+
} else {
160+
$class = $value->getClass();
161+
}
162+
163+
return $class;
164+
}
165+
}
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 InvalidParame 5D9F terTypeHintException 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