AutowirePass::autowireCalls()
last analyzed

Size

Total Lines 69
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 41
dl 0
loc 69
c 0
b 0
f 0
nc 219
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Config\Resource\ClassExistenceResource;
15
use Symfony\Component\DependencyInjection\Attribute\Autowire;
16
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
17
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
18
use Symfony\Component\DependencyInjection\Attribute\Lazy;
19
use Symfony\Component\DependencyInjection\Attribute\Target;
20
use Symfony\Component\DependencyInjection\ContainerBuilder;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\DependencyInjection\Definition;
23
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
24
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
25
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
26
use Symfony\Component\DependencyInjection\Reference;
27
use Symfony\Component\DependencyInjection\TypedReference;
28
use Symfony\Component\VarExporter\ProxyHelper;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\VarExporter\ProxyHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
30
/**
31
 * Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
32
 *
33
 * @author Kévin Dunglas <[email protected]>
34
 * @author Nicolas Grekas <[email protected]>
35
 */
36
class AutowirePass extends AbstractRecursivePass
37
{
38
    protected bool $skipScalars = true;
39
40
    private array $types;
41
    private array $ambiguousServiceTypes;
42
    private array $autowiringAliases;
43
    private ?string $lastFailure = null;
44
    private ?string $decoratedClass = null;
45
    private ?string $decoratedId = null;
46
    private object $defaultArgument;
47
    private ?\Closure $restorePreviousValue = null;
48
    private ?self $typesClone = null;
49
50
    public function __construct(
51
        private bool $throwOnAutowiringException = true,
52
    ) {
53
        $this->defaultArgument = new class {
54
            public $value;
55
            public $names;
56
            public $bag;
57
58
            public function withValue(\ReflectionParameter $parameter): self
59
            {
60
                $clone = clone $this;
61
                $clone->value = $this->bag->escapeValue($parameter->getDefaultValue());
62
63
                return $clone;
64
            }
65
        };
66
    }
67
68
    public function process(ContainerBuilder $container): void
69
    {
70
        $this->defaultArgument->bag = $container->getParameterBag();
71
72
        try {
73
            $this->typesClone = clone $this;
74
            parent::process($container);
75
        } finally {
76
            $this->decoratedClass = null;
77
            $this->decoratedId = null;
78
            $this->defaultArgument->bag = null;
79
            $this->defaultArgument->names = null;
80
            $this->restorePreviousValue = null;
81
            $this->typesClone = null;
82
        }
83
    }
84
85
    protected function processValue(mixed $value, bool $isRoot = false): mixed
86
    {
87
        if ($value instanceof Autowire) {
88
            return $this->processValue($this->container->getParameterBag()->resolveValue($value->value));
0 ignored issues
show
Bug introduced by
The method getParameterBag() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

88
            return $this->processValue($this->container->/** @scrutinizer ignore-call */ getParameterBag()->resolveValue($value->value));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
89
        }
90
91
        if ($value instanceof AutowireDecorated) {
92
            $definition = $this->container->getDefinition($this->currentId);
0 ignored issues
show
Bug introduced by
It seems like $this->currentId can also be of type null; however, parameter $id of Symfony\Component\Depend...uilder::getDefinition() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

92
            $definition = $this->container->getDefinition(/** @scrutinizer ignore-type */ $this->currentId);
Loading history...
93
94
            return new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE);
95
        }
96
97
        try {
98
            return $this->doProcessValue($value, $isRoot);
99
        } catch (AutowiringFailedException $e) {
100
            if ($this->throwOnAutowiringException) {
101
                throw $e;
102
            }
103
104
            $this->container->getDefinition($this->currentId)->addError($e->getMessageCallback() ?? $e->getMessage());
105
106
            return parent::processValue($value, $isRoot);
107
        }
108
    }
109
110
    private function doProcessValue(mixed $value, bool $isRoot = false): mixed
111
    {
112
        if ($value instanceof TypedReference) {
113
            foreach ($value->getAttributes() as $attribute) {
114
                if ($attribute === $v = $this->processValue($attribute)) {
115
                    continue;
116
                }
117
                if (!$attribute instanceof Autowire || !$v instanceof Reference) {
118
                    return $v;
119
                }
120
121
                $invalidBehavior = ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE !== $v->getInvalidBehavior() ? $v->getInvalidBehavior() : $value->getInvalidBehavior();
122
                $value = $v instanceof TypedReference
123
                    ? new TypedReference($v, $v->getType(), $invalidBehavior, $v->getName() ?? $value->getName(), array_merge($v->getAttributes(), $value->getAttributes()))
124
                    : new TypedReference($v, $value->getType(), $invalidBehavior, $value->getName(), $value->getAttributes());
125
                break;
126
            }
127
            if ($ref = $this->getAutowiredReference($value, true)) {
128
                return $ref;
129
            }
130
            if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
131
                $message = $this->createTypeNotFoundMessageCallback($value, 'it');
132
133
                // since the error message varies by referenced id and $this->currentId, so should the id of the dummy errored definition
134
                $this->container->register($id = \sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType())
135
                    ->addError($message);
136
137
                return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName());
138
            }
139
        }
140
        $value = parent::processValue($value, $isRoot);
141
142
        if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
143
            return $value;
144
        }
145
        if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
146
            $this->container->log($this, \sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
147
148
            return $value;
149
        }
150
151
        $methodCalls = $value->getMethodCalls();
152
153
        try {
154
            $constructor = $this->getConstructor($value, false);
155
        } catch (RuntimeException $e) {
156
            throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
0 ignored issues
show
Bug introduced by
It seems like $this->currentId can also be of type null; however, parameter $serviceId of Symfony\Component\Depend...xception::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
            throw new AutowiringFailedException(/** @scrutinizer ignore-type */ $this->currentId, $e->getMessage(), 0, $e);
Loading history...
157
        }
158
159
        if ($constructor) {
160
            array_unshift($methodCalls, [$constructor, $value->getArguments()]);
161
        }
162
163
        $checkAttributes = !$value->hasTag('container.ignore_attributes');
164
        $methodCalls = $this->autowireCalls($methodCalls, $reflectionClass, $isRoot, $checkAttributes);
165
166
        if ($constructor) {
167
            [, $arguments] = array_shift($methodCalls);
168
169
            if ($arguments !== $value->getArguments()) {
170
                $value->setArguments($arguments);
171
            }
172
        }
173
174
        if ($methodCalls !== $value->getMethodCalls()) {
175
            $value->setMethodCalls($methodCalls);
176
        }
177
178
        return $value;
179
    }
180
181
    private function autowireCalls(array $methodCalls, \ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
182
    {
183
        if ($isRoot) {
184
            $this->decoratedId = null;
185
            $this->decoratedClass = null;
186
            $this->restorePreviousValue = null;
187
188
            if (($definition = $this->container->getDefinition($this->currentId)) && null !== ($this->decoratedId = $definition->innerServiceId) && $this->container->has($this->decoratedId)) {
0 ignored issues
show
Bug introduced by
It seems like $this->currentId can also be of type null; however, parameter $id of Symfony\Component\Depend...uilder::getDefinition() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
            if (($definition = $this->container->getDefinition(/** @scrutinizer ignore-type */ $this->currentId)) && null !== ($this->decoratedId = $definition->innerServiceId) && $this->container->has($this->decoratedId)) {
Loading history...
189
                $this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
190
            }
191
        }
192
193
        $patchedIndexes = [];
194
195
        foreach ($methodCalls as $i => $call) {
196
            [$method, $arguments] = $call;
197
198
            if ($method instanceof \ReflectionFunctionAbstract) {
199
                $reflectionMethod = $method;
200
            } else {
201
                $definition = new Definition($reflectionClass->name);
202
                try {
203
                    $reflectionMethod = $this->getReflectionMethod($definition, $method);
204
                } catch (RuntimeException $e) {
205
                    if ($definition->getFactory()) {
206
                        continue;
207
                    }
208
                    throw $e;
209
                }
210
            }
211
212
            $arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes);
213
214
            if ($arguments !== $call[1]) {
215
                $methodCalls[$i][1] = $arguments;
216
                $patchedIndexes[] = $i;
217
            }
218
        }
219
220
        // use named arguments to skip complex default values
221
        foreach ($patchedIndexes as $i) {
222
            $namedArguments = null;
223
            $arguments = $methodCalls[$i][1];
224
225
            foreach ($arguments as $j => $value) {
226
                if ($namedArguments && !$value instanceof $this->defaultArgument) {
227
                    unset($arguments[$j]);
228
                    $arguments[$namedArguments[$j]] = $value;
229
                }
230
                if (!$value instanceof $this->defaultArgument) {
231
                    continue;
232
                }
233
234
                if (\is_array($value->value) ? $value->value : \is_object($value->value)) {
235
                    unset($arguments[$j]);
236
                    $namedArguments = $value->names;
237
                }
238
239
                if ($namedArguments) {
240
                    unset($arguments[$j]);
241
                } else {
242
                    $arguments[$j] = $value->value;
243
                }
244
            }
245
246
            $methodCalls[$i][1] = $arguments;
247
        }
248
249
        return $methodCalls;
250
    }
251
252
    /**
253
     * Autowires the constructor or a method.
254
     *
255
     * @throws AutowiringFailedException
256
     */
257
    private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes): array
258
    {
259
        $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
260
        $method = $reflectionMethod->name;
261
        $parameters = $reflectionMethod->getParameters();
262
        if ($reflectionMethod->isVariadic()) {
263
            array_pop($parameters);
264
        }
265
        $defaultArgument = clone $this->defaultArgument;
266
        $defaultArgument->names = new \ArrayObject();
267
268
        foreach ($parameters as $index => $parameter) {
269
            $defaultArgument->names[$index] = $parameter->name;
270
271
            if (\array_key_exists($parameter->name, $arguments)) {
272
                $arguments[$index] = $arguments[$parameter->name];
273
                unset($arguments[$parameter->name]);
274
            }
275
            if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
276
                continue;
277
            }
278
279
            $type = ProxyHelper::exportType($parameter, true);
280
            $target = null;
281
            $name = Target::parseName($parameter, $target);
282
            $target = $target ? [$target] : [];
283
            $currentId = $this->currentId;
284
285
            $getValue = function () use ($type, $parameter, $class, $method, $name, $target, $defaultArgument, $currentId) {
286
                if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $name, $target), false)) {
287
                    $failureMessage = $this->createTypeNotFoundMessageCallback($ref, \sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $currentId ? $class.'::'.$method : $method));
288
289
                    if ($parameter->isDefaultValueAvailable()) {
290
                        $value = $defaultArgument->withValue($parameter);
291
                    } elseif (!$parameter->allowsNull()) {
292
                        throw new AutowiringFailedException($currentId, $failureMessage);
0 ignored issues
show
Bug introduced by
It seems like $currentId can also be of type null; however, parameter $serviceId of Symfony\Component\Depend...xception::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

292
                        throw new AutowiringFailedException(/** @scrutinizer ignore-type */ $currentId, $failureMessage);
Loading history...
293
                    }
294
                }
295
296
                return $value;
297
            };
298
299
            if ($checkAttributes) {
300
                $attributes = array_merge($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF), $parameter->getAttributes(Lazy::class, \ReflectionAttribute::IS_INSTANCEOF));
301
302
                if (1 < \count($attributes)) {
303
                    throw new AutowiringFailedException($this->currentId, 'Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.');
0 ignored issues
show
Bug introduced by
It seems like $this->currentId can also be of type null; however, parameter $serviceId of Symfony\Component\Depend...xception::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

303
                    throw new AutowiringFailedException(/** @scrutinizer ignore-type */ $this->currentId, 'Using both attributes #[Lazy] and #[Autowire] on an argument is not allowed; use the "lazy" parameter of #[Autowire] instead.');
Loading history...
304
                }
305
306
                foreach ($attributes as $attribute) {
307
                    $attribute = $attribute->newInstance();
308
                    $value = $attribute instanceof Autowire ? $attribute->value : null;
309
310
                    if (\is_string($value) && str_starts_with($value, '%env(') && str_ends_with($value, ')%')) {
311
                        if ($parameter->getType() instanceof \ReflectionNamedType && 'bool' === $parameter->getType()->getName() && !str_starts_with($value, '%env(bool:')) {
312
                            $attribute = new Autowire(substr_replace($value, 'bool:', 5, 0));
313
                        }
314
                        if ($parameter->isDefaultValueAvailable() && $parameter->allowsNull() && null === $parameter->getDefaultValue() && !preg_match('/(^|:)default:/', $value)) {
315
                            $attribute = new Autowire(substr_replace($value, 'default::', 5, 0));
316
                        }
317
                    }
318
319
                    $invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE;
320
321
                    try {
322
                        $value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target]));
323
                    } catch (ParameterNotFoundException $e) {
324
                        if (!$parameter->isDefaultValueAvailable()) {
325
                            throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
326
                        }
327
                        $arguments[$index] = clone $defaultArgument;
328
                        $arguments[$index]->value = $parameter->getDefaultValue();
329
330
                        continue 2;
331
                    }
332
333
                    if ($attribute instanceof AutowireInline) {
334
                        $value = $attribute->buildDefinition($value, $type, $parameter);
335
                        $value = $this->doProcessValue($value);
336
                    } elseif ($lazy = $attribute->lazy) {
337
                        $definition = (new Definition($type))
338
                            ->setFactory('current')
339
                            ->setArguments([[$value ??= $getValue()]])
340
                            ->setLazy(true);
341
342
                        if (!\is_array($lazy)) {
343
                            if (str_contains($type, '|')) {
344
                                throw new AutowiringFailedException($this->currentId, \sprintf('Cannot use #[Autowire] with option "lazy: true" on union types for service "%s"; set the option to the interface(s) that should be proxied instead.', $this->currentId));
345
                            }
346
                            $lazy = str_contains($type, '&') ? explode('&', $type) : [];
347
                        }
348
349
                        if ($lazy) {
350
                            if (!class_exists($type) && !interface_exists($type, false)) {
351
                                $definition->setClass('object');
352
                            }
353
                            foreach ($lazy as $v) {
354
                                $definition->addTag('proxy', ['interface' => $v]);
355
                            }
356
                        }
357
358
                        if ($definition->getClass() !== (string) $value || $definition->getTag('proxy')) {
359
                            $value .= '.'.$this->container->hash([$definition->getClass(), $definition->getTag('proxy')]);
360
                        }
361
                        $this->container->setDefinition($value = '.lazy.'.$value, $definition);
362
                        $value = new Reference($value);
363
                    }
364
                    $arguments[$index] = $value;
365
366
                    continue 2;
367
                }
368
369
                foreach ($parameter->getAttributes(AutowireDecorated::class) as $attribute) {
370
                    $arguments[$index] = $this->processValue($attribute->newInstance());
371
372
                    continue 2;
373
                }
374
            }
375
376
            if (!$type) {
377
                if (isset($arguments[$index])) {
378
                    continue;
379
                }
380
381
                // no default value? Then fail
382
                if (!$parameter->isDefaultValueAvailable()) {
383
                    // For core classes, isDefaultValueAvailable() can
384
                    // be false when isOptional() returns true. If the
385
                    // argument *is* optional, allow it to be missing
386
                    if ($parameter->isOptional()) {
387
                        --$index;
388
                        break;
389
                    }
390
                    $type = ProxyHelper::exportType($parameter);
391
                    $type = $type ? \sprintf('is type-hinted "%s"', preg_replace('/(^|[(|&])\\\\|^\?\\\\?/', '\1', $type)) : 'has no type-hint';
392
393
                    throw new AutowiringFailedException($this->currentId, \sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type));
394
                }
395
396
                // specifically pass the default value
397
                $arguments[$index] = $defaultArgument->withValue($parameter);
398
399
                continue;
400
            }
401
402
            if ($this->decoratedClass && is_a($this->decoratedClass, $type, true)) {
403
                if ($this->restorePreviousValue) {
404
                    // The inner service is injected only if there is only 1 argument matching the type of the decorated class
405
                    // across all arguments of all autowired methods.
406
                    // If a second matching argument is found, the default behavior is restored.
407
                    ($this->restorePreviousValue)();
408
                    $this->decoratedClass = $this->restorePreviousValue = null; // Prevent further checks
409
                } else {
410
                    $arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
0 ignored issues
show
Bug introduced by
It seems like $this->decoratedId can also be of type null; however, parameter $id of Symfony\Component\Depend...eference::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

410
                    $arguments[$index] = new TypedReference(/** @scrutinizer ignore-type */ $this->decoratedId, $this->decoratedClass);
Loading history...
411
                    $argumentAtIndex = &$arguments[$index];
412
                    $this->restorePreviousValue = static function () use (&$argumentAtIndex, $getValue) {
413
                        $argumentAtIndex = $getValue();
414
                    };
415
416
                    continue;
417
                }
418
            }
419
420
            $arguments[$index] = $getValue();
421
        }
422
423
        if ($parameters && !isset($arguments[++$index])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $index seems to be defined by a foreach iteration on line 268. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
424
            while (0 <= --$index) {
425
                if (!$arguments[$index] instanceof $defaultArgument) {
426
                    break;
427
                }
428
                unset($arguments[$index]);
429
            }
430
        }
431
432
        // it's possible index 1 was set, then index 0, then 2, etc
433
        // make sure that we re-order so they're injected as expected
434
        ksort($arguments, \SORT_NATURAL);
435
436
        return $arguments;
437
    }
438
439
    /**
440
     * Returns a reference to the service matching the given type, if any.
441
     */
442
    private function getAutowiredReference(TypedReference $reference, bool $filterType): ?TypedReference
443
    {
444
        $this->lastFailure = null;
445
        $type = $reference->getType();
446
447
        if ($type !== (string) $reference) {
448
            return $reference;
449
        }
450
451
        if ($filterType && false !== $m = strpbrk($type, '&|')) {
452
            $types = array_diff(explode($m[0], $type), ['int', 'string', 'array', 'bool', 'float', 'iterable', 'object', 'callable', 'null']);
453
454
            sort($types);
455
456
            $type = implode($m[0], $types);
457
        }
458
459
        $name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name;
460
461
        if (null !== $name ??= $reference->getName()) {
462
            if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
463
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
464
            }
465
466
            if (null !== ($alias = $this->getCombinedAlias($type, $name)) && !$this->container->findDefinition($alias)->isAbstract()) {
467
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
468
            }
469
470
            $parsedName = (new Target($name))->getParsedName();
471
472
            if ($this->container->has($alias = $type.' $'.$parsedName) && !$this->container->findDefinition($alias)->isAbstract()) {
473
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
474
            }
475
476
            if (null !== ($alias = $this->getCombinedAlias($type, $parsedName)) && !$this->container->findDefinition($alias)->isAbstract()) {
477
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
478
            }
479
480
            if (($this->container->has($n = $name) && !$this->container->findDefinition($n)->isAbstract())
0 ignored issues
show
Bug introduced by
It seems like $n = $name can also be of type null; however, parameter $id of Symfony\Component\Depend...ContainerBuilder::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

480
            if (($this->container->has(/** @scrutinizer ignore-type */ $n = $name) && !$this->container->findDefinition($n)->isAbstract())
Loading history...
Bug introduced by
It seems like $n can also be of type null; however, parameter $id of Symfony\Component\Depend...ilder::findDefinition() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

480
            if (($this->container->has($n = $name) && !$this->container->findDefinition(/** @scrutinizer ignore-type */ $n)->isAbstract())
Loading history...
481
                || ($this->container->has($n = $parsedName) && !$this->container->findDefinition($n)->isAbstract())
482
            ) {
483
                foreach ($this->container->getAliases() as $id => $alias) {
484
                    if ($n === (string) $alias && str_starts_with($id, $type.' $')) {
485
                        return new TypedReference($n, $type, $reference->getInvalidBehavior());
486
                    }
487
                }
488
            }
489
490
            if (null !== $target) {
491
                return null;
492
            }
493
        }
494
495
        if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
496
            return new TypedReference($type, $type, $reference->getInvalidBehavior());
497
        }
498
499
        if (null !== ($alias = $this->getCombinedAlias($type)) && !$this->container->findDefinition($alias)->isAbstract()) {
500
            return new TypedReference($alias, $type, $reference->getInvalidBehavior());
501
        }
502
503
        return null;
504
    }
505
506
    /**
507
     * Populates the list of available types.
508
     */
509
    private function populateAvailableTypes(ContainerBuilder $container): void
510
    {
511
        $this->types = [];
512
        $this->ambiguousServiceTypes = [];
513
        $this->autowiringAliases = [];
514
515
        foreach ($container->getDefinitions() as $id => $definition) {
516
            $this->populateAvailableType($container, $id, $definition);
517
        }
518
519
        $prev = null;
520
        foreach ($container->getAliases() as $id => $alias) {
521
            $this->populateAutowiringAlias($id, $prev);
522
            $prev = $id;
523
        }
524
    }
525
526
    /**
527
     * Populates the list of available types for a given definition.
528
     */
529
    private function populateAvailableType(ContainerBuilder $container, string $id, Definition $definition): void
530
    {
531
        // Never use abstract services
532
        if ($definition->isAbstract()) {
533
            return;
534
        }
535
536
        if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass = $container->getReflectionClass($definition->getClass(), false)) {
537
            return;
538
        }
539
540
        foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
541
            $this->set($reflectionInterface->name, $id);
542
        }
543
544
        do {
545
            $this->set($reflectionClass->name, $id);
546
        } while ($reflectionClass = $reflectionClass->getParentClass());
547
548
        $this->populateAutowiringAlias($id);
549
    }
550
551
    /**
552
     * Associates a type and a service id if applicable.
553
     */
554
    private function set(string $type, string $id): void
555
    {
556
        // is this already a type/class that is known to match multiple services?
557
        if (isset($this->ambiguousServiceTypes[$type])) {
558
            $this->ambiguousServiceTypes[$type][] = $id;
559
560
            return;
561
        }
562
563
        // check to make sure the type doesn't match multiple services
564
        if (!isset($this->types[$type]) || $this->types[$type] === $id) {
565
            $this->types[$type] = $id;
566
567
            return;
568
        }
569
570
        // keep an array of all services matching this type
571
        if (!isset($this->ambiguousServiceTypes[$type])) {
572
            $this->ambiguousServiceTypes[$type] = [$this->types[$type]];
573
            unset($this->types[$type]);
574
        }
575
        $this->ambiguousServiceTypes[$type][] = $id;
576
    }
577
578
    private function createTypeNotFoundMessageCallback(TypedReference $reference, string $label): \Closure
579
    {
580
        if (!isset($this->typesClone->container)) {
581
            $this->typesClone->container = new ContainerBuilder($this->container->getParameterBag());
582
            $this->typesClone->container->setAliases($this->container->getAliases());
0 ignored issues
show
Bug introduced by
The method setAliases() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

582
            $this->typesClone->container->/** @scrutinizer ignore-call */ 
583
                                          setAliases($this->container->getAliases());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
583
            $this->typesClone->container->setDefinitions($this->container->getDefinitions());
584
            $this->typesClone->container->setResourceTracking(false);
585
        }
586
        $currentId = $this->currentId;
587
588
        return (fn () => $this->createTypeNotFoundMessage($reference, $label, $currentId))->bindTo($this->typesClone);
0 ignored issues
show
Bug introduced by
It seems like $currentId can also be of type null; however, parameter $currentId of Symfony\Component\Depend...teTypeNotFoundMessage() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

588
        return (fn () => $this->createTypeNotFoundMessage($reference, $label, /** @scrutinizer ignore-type */ $currentId))->bindTo($this->typesClone);
Loading history...
589
    }
590
591
    private function createTypeNotFoundMessage(TypedReference $reference, string $label, string $currentId): string
592
    {
593
        $type = $reference->getType();
594
595
        $i = null;
596
        $namespace = $type;
597
        do {
598
            $namespace = substr($namespace, 0, $i);
599
600
            if ($this->container->hasDefinition($namespace) && $tag = $this->container->getDefinition($namespace)->getTag('container.excluded')) {
601
                return \sprintf('Cannot autowire service "%s": %s needs an instance of "%s" but this type has been excluded %s.', $currentId, $label, $type, $tag[0]['source'] ?? 'from autowiring');
602
            }
603
        } while (false !== $i = strrpos($namespace, '\\'));
604
605
        if (!$r = $this->container->getReflectionClass($type, false)) {
606
            // either $type does not exist or a parent class does not exist
607
            try {
608
                if (class_exists(ClassExistenceResource::class)) {
609
                    $resource = new ClassExistenceResource($type, false);
610
                    // isFresh() will explode ONLY if a parent class/trait does not exist
611
                    $resource->isFresh(0);
612
                    $parentMsg = false;
613
                } else {
614
                    $parentMsg = "couldn't be loaded. Either it was not found or it is missing a parent class or a trait";
615
                }
616
            } catch (\ReflectionException $e) {
617
                $parentMsg = \sprintf('is missing a parent class (%s)', $e->getMessage());
618
            }
619
620
            $message = \sprintf('has type "%s" but this class %s.', $type, $parentMsg ?: 'was not found');
621
        } else {
622
            $alternatives = $this->createTypeAlternatives($this->container, $reference);
0 ignored issues
show
Bug introduced by
It seems like $this->container can also be of type null; however, parameter $container of Symfony\Component\Depend...reateTypeAlternatives() does only seem to accept Symfony\Component\Depend...ection\ContainerBuilder, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

622
            $alternatives = $this->createTypeAlternatives(/** @scrutinizer ignore-type */ $this->container, $reference);
Loading history...
623
624
            if (null !== $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)) {
625
                $target = null !== $target->name ? "('{$target->name}')" : '';
626
                $message = \sprintf('has "#[Target%s]" but no such target exists.%s', $target, $alternatives);
627
            } else {
628
                $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
629
                $message = \sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
630
            }
631
632
            if ($r->isInterface() && !$alternatives) {
633
                $message .= ' Did you create an instantiable class that implements this interface?';
634
            }
635
        }
636
637
        $message = \sprintf('Cannot autowire service "%s": %s %s', $currentId, $label, $message);
638
639
        if (null !== $this->lastFailure) {
640
            $message = $this->lastFailure."\n".$message;
641
            $this->lastFailure = null;
642
        }
643
644
        return $message;
645
    }
646
647
    private function createTypeAlternatives(ContainerBuilder $container, TypedReference $reference): string
648
    {
649
        // try suggesting available aliases first
650
        if ($message = $this->getAliasesSuggestionForType($container, $type = $reference->getType())) {
651
            return ' '.$message;
652
        }
653
        if (!isset($this->ambiguousServiceTypes)) {
654
            $this->populateAvailableTypes($container);
655
        }
656
657
        $servicesAndAliases = $container->getServiceIds();
658
        $autowiringAliases = $this->autowiringAliases[$type] ?? [];
659
        unset($autowiringAliases['']);
660
661
        if ($autowiringAliases) {
662
            return \sprintf(' Did you mean to target%s "%s" instead?', 1 < \count($autowiringAliases) ? ' one of' : '', implode('", "', $autowiringAliases));
663
        }
664
665
        if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) {
666
            return \sprintf(' Did you mean "%s"?', $servicesAndAliases[$key]);
667
        } elseif (isset($this->ambiguousServiceTypes[$type])) {
668
            $message = \sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type]));
669
        } elseif (isset($this->types[$type])) {
670
            $message = \sprintf('the existing "%s" service', $this->types[$type]);
671
        } else {
672
            return '';
673
        }
674
675
        return \sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message);
676
    }
677
678
    private function getAliasesSuggestionForType(ContainerBuilder $container, string $type): ?string
679
    {
680
        $aliases = [];
681
        foreach (class_parents($type) + class_implements($type) as $parent) {
682
            if ($container->has($parent) && !$container->findDefinition($parent)->isAbstract()) {
683
                $aliases[] = $parent;
684
            }
685
        }
686
687
        if (1 < $len = \count($aliases)) {
688
            $message = 'Try changing the type-hint to one of its parents: ';
689
            for ($i = 0, --$len; $i < $len; ++$i) {
690
                $message .= \sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
691
            }
692
693
            return $message.\sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
694
        }
695
696
        if ($aliases) {
697
            return \sprintf('Try changing the type-hint to "%s" instead.', $aliases[0]);
698
        }
699
700
        return null;
701
    }
702
703
    private function populateAutowiringAlias(string $id, ?string $target = null): void
704
    {
705
        if (!preg_match('/(?(DEFINE)(?<V>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) {
706
            return;
707
        }
708
709
        $type = $m[2];
710
        $name = $m[3] ?? '';
711
712
        if (class_exists($type, false) || interface_exists($type, false)) {
713
            if (null !== $target && str_starts_with($target, '.'.$type.' $')
714
                && (new Target($target = substr($target, \strlen($type) + 3)))->getParsedName() === $name
715
            ) {
716
                $name = $target;
717
            }
718
719
            $this->autowiringAliases[$type][$name] = $name;
720
        }
721
    }
722
723
    private function getCombinedAlias(string $type, ?string $name = null): ?string
724
    {
725
        if (str_contains($type, '&')) {
726
            $types = explode('&', $type);
727
        } elseif (str_contains($type, '|')) {
728
            $types = explode('|', $type);
729
        } else {
730
            return null;
731
        }
732
733
        $alias = null;
734
        $suffix = $name ? ' $'.$name : '';
735
736
        foreach ($types as $type) {
737
            if (!$this->container->hasAlias($type.$suffix)) {
738
                return null;
739
            }
740
741
            if (null === $alias) {
742
                $alias = (string) $this->container->getAlias($type.$suffix);
743
            } elseif ((string) $this->container->getAlias($type.$suffix) !== $alias) {
744
                return null;
745
            }
746
        }
747
748
        return $alias;
749
    }
750
}
751