Passed
Pull Request — master (#64)
by Aleksei
07:27 queued 05:18
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 ReflectionType $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 NotFoundExceptionInterface|null $error Last caught {@see NotFoundExceptionInterface} exception.
232
     *
233
     * @throws ContainerExceptionInterface
234
     *
235
     * @return bool {@see true} if argument was resolved
236
     *
237
     * @psalm-suppress MixedAssignment
238
     * @psalm-suppress PossiblyUndefinedMethod
239
     */
240 32
    private function resolveParameterType(
241
        ResolvingState $state,
242
        ReflectionType $type,
243
        bool $variadic,
244
        ?NotFoundExceptionInterface &$error
245
    ): bool {
246
        switch (true) {
247 32
            case $type instanceof ReflectionNamedType:
248 31
                $types = [$type];
249
                // no break
250 1
            case $type instanceof ReflectionUnionType:
251 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...
252
                /** @var array<int, ReflectionNamedType> $types */
253 32
                foreach ($types as $namedType) {
254
                    try {
255 32
                        if ($this->resolveNamedType($state, $namedType, $variadic)) {
256 31
                            return true;
257
                        }
258 6
                    } catch (NotFoundExceptionInterface $e) {
259 6
                        $error = $e;
260
                    }
261
                }
262 13
                break;
263
            case $type instanceof ReflectionIntersectionType:
264
                $classes = [];
265
                /** @var ReflectionNamedType $namedType */
266
                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

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