Skip to content

Commit 48d5245

Browse files
feature #32256 [DI] Add compiler pass and command to check that services wiring matches type declarations (alcalyn, GuilhemN, nicolas-grekas)
This PR was merged into the 4.4 branch. Discussion ---------- [DI] Add compiler pass and command to check that services wiring matches type declarations | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #27744 | License | MIT | Doc PR | PR replacing symfony/symfony#27825. It adds a `lint:container` command asserting the type hints used in your code are correct. Commits ------- 8230a1543e Make it really work on real apps 4b3e9d4c96 Fix comments, improve the feature a6292b917b [DI] Add compiler pass to check arguments type hint
2 parents 593db85 + 76865e5 commit 48d5245

File tree

10 files changed

+871
-0
lines changed

10 files changed

+871
-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 `CheckTypeDeclarationsPass` 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/AbstractRecursivePass.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,12 @@ protected function getConstructor(Definition $definition, $required)
133133
list($class, $method) = $factory;
134134
if ($class instanceof Reference) {
135135
$class = $this->container->findDefinition((string) $class)->getClass();
136+
} elseif ($class instanceof Definition) {
137+
$class = $class->getClass();
136138
} elseif (null === $class) {
137139
$class = $definition->getClass();
138140
}
141+
139142
if ('__construct' === $method) {
140143
throw new RuntimeException(sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId));
141144
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Container;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
19+
use Symfony\Component\DependencyInjection\Parameter;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Component\DependencyInjection\ServiceLocator;
22+
23+
/**
24+
* Checks whether injected parameters are compatible with type declarations.
25+
*
26+
* This pass should be run after all optimization passes.
27+
*
28+
* It can be added either:
29+
* * before removing passes to check all services even if they are not currently used,
30+
* * after removing passes to check only services are used in the app.
31+
*
32+
* @author Nicolas Grekas <[email protected]>
33+
* @author Julien Maulny <[email protected]>
34+
*/
35+
final class CheckTypeDeclarationsPass extends AbstractRecursivePass
36+
{
37+
private const SCALAR_TYPES = ['int', 'float', 'bool', 'string'];
38+
39+
private $autoload;
40+
41+
/**
42+
* @param bool $autoload Whether services who's class in not loaded should be checked or not.
43+
* Defaults to false to save loading code during compilation.
44+
*/
45+
public function __construct(bool $autoload = false)
46+
{
47+
$this->autoload = $autoload;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
protected function processValue($value, $isRoot = false)
54+
{
55+
if (!$value instanceof Definition) {
56+
return parent::processValue($value, $isRoot);
57+
}
58+
59+
if (!$this->autoload && !class_exists($class = $value->getClass(), false) && !interface_exists($class, false)) {
60+
return parent::processValue($value, $isRoot);
61+
}
62+
63+
if (ServiceLocator::class === $value->getClass()) {
64+
return parent::processValue($value, $isRoot);
65+
}
66+
67+
if ($constructor = $this->getConstructor($value, false)) {
68+
$this->checkTypeDeclarations($value, $constructor, $value->getArguments());
69+
}
70+
71+
foreach ($value->getMethodCalls() as $methodCall) {
72+
$reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
73+
74+
$this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]);
75+
}
76+
77+
return parent::processValue($value, $isRoot);
78+
}
79+
80+
/**
81+
* @throws InvalidArgumentException When not enough parameters are defined for the method
82+
*/
83+
private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void
84+
{
85+
$numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
86+
87+
if (\count($values) < $numberOfRequiredParameters) {
88+
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values)));
89+
}
90+
91+
$reflectionParameters = $reflectionFunction->getParameters();
92+
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));
93+
94+
for ($i = 0; $i < $checksCount; ++$i) {
95+
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
96+
continue;
97+
}
98+
99+
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]);
100+
}
101+
102+
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
103+
$variadicParameters = \array_slice($values, $lastParameter->getPosition());
104+
105+
foreach ($variadicParameters as $variadicParameter) {
106+
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter);
107+
}
108+
}
109+
}
110+
111+
/**
112+
* @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
113+
*/
114+
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void
115+
{
116+
$type = $parameter->getType()->getName();
117+
118+
if ($value instanceof Reference) {
119+
if (!$this->container->has($value = (string) $value)) {
120+
return;
121+
}
122+
123+
if ('service_container' === $value && is_a($type, Container::class, true)) {
124+
return;
125+
}
126+
127+
$value = $this->container->findDefinition($value);
128+
}
129+
130+
if ('self' === $type) {
131+
$type = $parameter->getDeclaringClass()->getName();
132+
}
133+
134+
if ('static' === $type) {
135+
$type = $checkedDefinition->getClass();
136+
}
137+
138+
if ($value instanceof Definition) {
139+
$class = $value->getClass();
140+
141+
if (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) {
142+
return;
143+
}
144+
145+
if ('callable' === $type && method_exists($class, '__invoke')) {
146+
return;
147+
}
148+
149+
if ('iterable' === $type && is_subclass_of($class, 'Traversable')) {
150+
return;
151+
}
152+
153+
if (is_a($class, $type, true)) {
154+
return;
155+
}
156+
157+
throw new InvalidParameterTypeException($this->currentId, $class, $parameter);
158+
}
159+
160+
if ($value instanceof Parameter) {
161+
$value = $this->container->getParameter($value);
162+
} elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
163+
$value = $this->container->getParameter($match[1]);
164+
}
165+
166+
if (null === $value && $parameter->allowsNull()) {
167+
return;
168+
}
169+
170+
if (\in_array($type, self::SCALAR_TYPES, true) && is_scalar($value)) {
171+
return;
172+
}
173+
174+
if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition)) {
175+
return;
176+
}
177+
178+
if ('iterable' === $type && (\is_array($value) || $value instanceof \Traversable || $value instanceof IteratorArgument)) {
179+
return;
180+
}
181+
182+
if ('Traversable' === $type && ($value instanceof \Traversable || $value instanceof IteratorArgument)) {
183+
return;
184+
}
185+
186+
$checkFunction = sprintf('is_%s', $parameter->getType()->getName());
187+
188+
if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) {
189+
throw new InvalidParameterTypeException($this->currentId, \gettype($value), $parameter);
190+
}
191+
}
192+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 with an incompatible type.
16+
*
17+
* @author Nicolas Grekas <[email protected]>
18+
* @author Julien Maulny <[email protected]>
19+
*/
20+
class InvalidParameterTypeException extends InvalidArgumentException
21+
{
22+
public function __construct(string $serviceId, string $type, \ReflectionParameter $parameter)
23+
{
24+
parent::__construct(sprintf('Invalid definition for service "%s": argument %d of "%s::%s" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $type));
25+
}
26+
}

0 commit comments

Comments
 (0)