Passed
Pull Request — master (#64)
by Aleksei
20:11
created

Injector::resolveParameter()   B

Complexity

Conditions 11
Paths 14

Size

Total Lines 49
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11.0061

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 26
c 2
b 0
f 0
nc 14
nop 2
dl 0
loc 49
ccs 26
cts 27
cp 0.963
crap 11.0061
rs 7.3166

How to fix   Complexity   

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

267
                foreach ($type->/** @scrutinizer ignore-call */ getTypes() as $namedType) {
Loading history...
268
                    $classes[] = $namedType->getName();
269
                }
270
                /** @var array<int, class-string> $classes */
271
                if ($state->resolveParameterByClasses($classes, $variadic)) {
272
                    return true;
273
                }
274
                break;
275
        }
276 13
        return false;
277
    }
278
279
    /**
280
     * @throws ContainerExceptionInterface
281
     * @throws NotFoundExceptionInterface
282
     *
283
     * @return bool {@see true} if argument was resolved
284
     */
285 32
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic): bool
286
    {
287 32
        $type = $parameter->getName();
288
        /** @psalm-var class-string|null $class */
289 32
        $class = $parameter->isBuiltin() ? null : $type;
290 32
        $isClass = $class !== null || $type === 'object';
291 32
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic);
292
    }
293
294
    /**
295
     * @psalm-param class-string|null $class
296
     *
297
     * @throws ContainerExceptionInterface
298
     * @throws NotFoundExceptionInterface
299
     *
300
     * @return bool True if argument resolved
301
     */
302 27
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic): bool
303
    {
304 27
        $found = $state->resolveParameterByClass($class, $isVariadic);
305 27
        if ($found || $isVariadic) {
306 13
            return $found;
307
        }
308 20
        if ($class !== null) {
309
            /** @var mixed $argument */
310 19
            $argument = $this->container->get($class);
311 15
            $state->addResolvedValue($argument);
312 15
            return true;
313
        }
314 1
        return false;
315
    }
316
}
317