Passed
Push — master ( 1f7239...9b4fbd )
by Sergei
02:58
created

Injector   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Test Coverage

Coverage 86.87%

Importance

Changes 14
Bugs 0 Features 1
Metric Value
eloc 95
c 14
b 0
f 1
dl 0
loc 318
ccs 86
cts 99
cp 0.8687
rs 8.96
wmc 43

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A withCacheReflections() 0 5 1
B resolveParameterType() 0 36 9
B resolveParameter() 0 48 11
A resolveDependencies() 0 29 6
A resolveNamedType() 0 7 4
A invoke() 0 5 1
A make() 0 13 3
A getClassReflection() 0 7 2
A resolveObjectParameter() 0 12 5

How to fix   Complexity   

Complex Class

Complex classes like Injector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Injector, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Injector;
6
7
use Closure;
8
use Psr\Container\ContainerExceptionInterface;
9
use Psr\Container\ContainerInterface;
10
use Psr\Container\NotFoundExceptionInterface;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionFunction;
14
use ReflectionFunctionAbstract;
15
use ReflectionIntersectionType;
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType 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...
16
use ReflectionNamedType;
17
use ReflectionParameter;
18
use ReflectionType;
19
use ReflectionUnionType;
20
21
/**
22
 * Injector is able to analyze callable dependencies based on type hinting and
23
 * inject them from any PSR-11 compatible container.
24
 */
25
final class Injector
26
{
27
    private ?ContainerInterface $container;
28
    private bool $cacheReflections = false;
29
30
    /**
31
     * @var ReflectionClass[]
32
     * @psalm-var array<class-string,ReflectionClass>
33
     */
34
    private array $reflectionsCache = [];
35
36 61
    public function __construct(?ContainerInterface $container = null)
37
    {
38 61
        $this->container = $container;
39
    }
40
41
    /**
42
     * Enable memoization of class reflections for improved performance when resolving the same objects multiple times.
43
     * Note: Enabling this feature may increase memory usage.
44
     */
45 2
    public function withCacheReflections(bool $cacheReflections = true): self
46
    {
47 2
        $new = clone $this;
48 2
        $new->cacheReflections = $cacheReflections;
49 2
        return $new;
50
    }
51
52
    /**
53
     * Invoke a callback with resolving dependencies based on parameter types.
54
     *
55
     * This methods allows invoking a callback and let type hinted parameter names to be
56
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
57
     *
58
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
59
     *
60
     * ```php
61
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
62
     *    // ...
63
     * }
64
     *
65
     * $injector = new Yiisoft\Injector\Injector($container);
66
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
67
     * ```
68
     *
69
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
70
     * by the DI container as the second argument.
71
     *
72
     * @param callable $callable callable to be invoked.
73
     * @param array $arguments The array of the function arguments.
74
     * This can be either a list of arguments, or an associative array where keys are argument names.
75
     *
76
     * @throws MissingRequiredArgumentException if required argument is missing.
77
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
78
     * @throws ReflectionException
79
     *
80
     * @return mixed The callable return value.
81
     */
82 41
    public function invoke(callable $callable, array $arguments = [])
83
    {
84 41
        $callable = Closure::fromCallable($callable);
85 41
        $reflection = new ReflectionFunction($callable);
86 41
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
87
    }
88
89
    /**
90
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
91
     *
92
     * This methods allows invoking a constructor and let type hinted parameter names to be
93
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
94
     *
95
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
96
     *
97
     * ```php
98
     * class StringFormatter
99
     * {
100
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
101
     *     {
102
     *         // ...
103
     *     }
104
     * }
105
     *
106
     * $injector = new Yiisoft\Injector\Injector($container);
107
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
108
     * ```
109
     *
110
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
111
     * by the DI container as the second argument.
112
     *
113
     * @param string $class name of the class to be created.
114
     * @param array $arguments The array of the function arguments.
115
     * This can be either a list of arguments, or an associative array where keys are argument names.
116
     *
117
     * @throws ContainerExceptionInterface
118
     * @throws InvalidArgumentException|MissingRequiredArgumentException
119
     * @throws ReflectionException
120
     *
121
     * @return object The object of the given class.
122
     *
123
     * @psalm-suppress MixedMethodCall
124
     *
125
     * @psalm-template T
126
     * @psalm-param class-string<T> $class
127
     * @psalm-return T
128
     */
129 19
    public function make(string $class, array $arguments = []): object
130
    {
131 19
        $classReflection = $this->getClassReflection($class);
132 18
        if (!$classReflection->isInstantiable()) {
133 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
134
        }
135 15
        $reflection = $classReflection->getConstructor();
136 15
        if ($reflection === null) {
137
            // Method __construct() does not exist
138 2
            return new $class();
139
        }
140
141 13
        return new $class(...$this->resolveDependencies($reflection, $arguments));
142
    }
143
144
    /**
145
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
146
     * and return array of arguments to call the function with.
147
     *
148
     * @param ReflectionFunctionAbstract $reflection function reflection.
149
     * @param array $arguments concrete arguments.
150
     *
151
     * @throws ContainerExceptionInterface
152
     * @throws InvalidArgumentException|MissingRequiredArgumentException
153
     * @throws ReflectionException
154
     *
155
     * @return array resolved arguments.
156
     */
157 54
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
158
    {
159 54
        $state = new ResolvingState($reflection, $arguments);
160
161 51
        $isInternalOptional = false;
162 51
        $internalParameter = '';
163 51
        foreach ($reflection->getParameters() as $parameter) {
164 43
            if ($isInternalOptional) {
165
                // Check custom parameter definition for an internal function
166
                if ($state->hasNamedArgument($parameter->getName())) {
167
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
168
                }
169
                continue;
170
            }
171
            // Resolve parameter
172 43
            $resolved = $this->resolveParameter($parameter, $state);
173 43
            if ($resolved === true) {
174 39
                continue;
175
            }
176
177 6
            if ($resolved === false) {
178 6
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
179
            }
180
            // Internal function. Parameter not resolved
181
            $isInternalOptional = true;
182
            $internalParameter = $parameter->getName();
183
        }
184
185 44
        return $state->getResolvedValues();
186
    }
187
188
    /**
189
     * @throws NotFoundExceptionInterface
190
     * @throws ReflectionException
191
     *
192
     * @return bool|null True if argument resolved; False if not resolved; Null if parameter is optional but
193
     * without default value in a Reflection object. This is possible for internal functions.
194
     */
195 43
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state): ?bool
196
    {
197 43
        $name = $parameter->getName();
198 43
        $isVariadic = $parameter->isVariadic();
199 43
        $hasType = $parameter->hasType();
200 43
        $state->disablePushTrailingArguments($isVariadic && $hasType);
201
202
        // Try to resolve parameter by name
203 43
        if ($state->resolveParameterByName($name, $isVariadic)) {
204 14
            return true;
205
        }
206
207 37
        $error = null;
208
209 37
        if ($hasType) {
210
            /** @var ReflectionType $reflectionType */
211 36
            $reflectionType = $parameter->getType();
212
213 36
            if ($this->resolveParameterType($state, $reflectionType, $isVariadic, $error)) {
214 24
                return true;
215
            }
216
        }
217
218 19
        if ($parameter->isDefaultValueAvailable()) {
219 7
            $argument = $parameter->getDefaultValue();
220 7
            $state->addResolvedValue($argument);
221 7
            return true;
222
        }
223
224 12
        if (!$parameter->isOptional()) {
225 11
            if ($hasType && $parameter->allowsNull()) {
226 4
                $argument = null;
227 4
                $state->addResolvedValue($argument);
228 4
                return true;
229
            }
230
231 7
            if ($error === null) {
232 6
                return false;
233
            }
234
235
            // Throw NotFoundExceptionInterface
236 1
            throw $error;
237
        }
238
239 1
        if ($isVariadic) {
240 1
            return true;
241
        }
242
        return null;
243
    }
244
245
    /**
246
     * Resolve parameter using its type.
247
     *
248
     * @param NotFoundExceptionInterface|null $error Last caught {@see NotFoundExceptionInterface} exception.
249
     *
250
     * @throws ContainerExceptionInterface
251
     *
252
     * @return bool True if argument was resolved
253
     *
254
     * @psalm-suppress PossiblyUndefinedMethod
255
     */
256 36
    private function resolveParameterType(
257
        ResolvingState $state,
258
        ReflectionType $type,
259
        bool $variadic,
260
        ?NotFoundExceptionInterface &$error
261
    ): bool {
262
        switch (true) {
263 36
            case $type instanceof ReflectionNamedType:
264 35
                $types = [$type];
265
                // no break
266 1
            case $type instanceof ReflectionUnionType:
267 36
                $types ??= $type->getTypes();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $types does not seem to be defined for all execution paths leading up to this point.
Loading history...
268
                /** @var array<int, ReflectionNamedType> $types */
269 36
                foreach ($types as $namedType) {
270
                    try {
271 36
                        if ($this->resolveNamedType($state, $namedType, $variadic)) {
272 35
                            return true;
273
                        }
274 6
                    } catch (NotFoundExceptionInterface $e) {
275 6
                        $error = $e;
276
                    }
277
                }
278 16
                break;
279
            case $type instanceof ReflectionIntersectionType:
280
                $classes = [];
281
                /** @var ReflectionNamedType $namedType */
282
                foreach ($type->getTypes() as $namedType) {
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

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

282
                foreach ($type->/** @scrutinizer ignore-call */ getTypes() as $namedType) {
Loading history...
283
                    $classes[] = $namedType->getName();
284
                }
285
                /** @var array<int, class-string> $classes */
286
                if ($state->resolveParameterByClasses($classes, $variadic)) {
287
                    return true;
288
                }
289
                break;
290
        }
291 16
        return false;
292
    }
293
294
    /**
295
     * @throws ContainerExceptionInterface
296
     * @throws NotFoundExceptionInterface
297
     *
298
     * @return bool True if argument was resolved
299
     */
300 36
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic): bool
301
    {
302 36
        $type = $parameter->getName();
303
        /** @psalm-var class-string|null $class */
304 36
        $class = $parameter->isBuiltin() ? null : $type;
305 36
        $isClass = $class !== null || $type === 'object';
306 36
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic);
307
    }
308
309
    /**
310
     * @psalm-param class-string|null $class
311
     *
312
     * @throws ContainerExceptionInterface
313
     * @throws NotFoundExceptionInterface
314
     *
315
     * @return bool True if argument resolved
316
     */
317 30
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic): bool
318
    {
319 30
        $found = $state->resolveParameterByClass($class, $isVariadic);
320 30
        if ($found || $isVariadic) {
321 13
            return $found;
322
        }
323 23
        if ($class !== null && $this->container !== null) {
324 20
            $argument = $this->container->get($class);
325 16
            $state->addResolvedValue($argument);
326 16
            return true;
327
        }
328 3
        return false;
329
    }
330
331
    /**
332
     * @psalm-param class-string $class
333
     *
334
     * @throws ReflectionException
335
     */
336 19
    private function getClassReflection(string $class): ReflectionClass
337
    {
338 19
        if ($this->cacheReflections) {
339 1
            return $this->reflectionsCache[$class] ??= new ReflectionClass($class);
340
        }
341
342 18
        return new ReflectionClass($class);
343
    }
344
}
345