Passed
Push — master ( 5c0805...0582ad )
by Alexander
01:15
created

Injector::resolveParameter()   F

Complexity

Conditions 18
Paths 240

Size

Total Lines 73
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 18.0405

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 41
c 1
b 0
f 0
nc 240
nop 4
dl 0
loc 73
ccs 38
cts 40
cp 0.95
crap 18.0405
rs 3.5333

How to fix   Long Method    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 Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Psr\Container\NotFoundExceptionInterface;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionFunctionAbstract;
13
use ReflectionParameter;
14
15
/**
16
 * Injector is able to analyze callable dependencies based on
17
 * type hinting and inject them from any PSR-11 compatible container.
18
 */
19
final class Injector
20
{
21
    private ContainerInterface $container;
22
23 45
    public function __construct(ContainerInterface $container)
24
    {
25 45
        $this->container = $container;
26 45
    }
27
28
    /**
29
     * Invoke a callback with resolving dependencies based on parameter types.
30
     *
31
     * This methods allows invoking a callback and let type hinted parameter names to be
32
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
33
     *
34
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
35
     *
36
     * ```php
37
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
38
     *    // ...
39
     * }
40
     *
41
     * $injector = new Yiisoft\Injector\Injector($container);
42
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
43
     * ```
44
     *
45
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
46
     * by the DI container as the second argument.
47
     *
48
     * @param callable $callable callable to be invoked.
49
     * @param array $arguments The array of the function arguments.
50
     * This can be either a list of arguments, or an associative array where keys are argument names.
51
     * @return mixed the callable return value.
52
     * @throws MissingRequiredArgumentException if required argument is missing.
53
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
54
     * @throws ReflectionException
55
     */
56 34
    public function invoke(callable $callable, array $arguments = [])
57
    {
58 34
        $callable = \Closure::fromCallable($callable);
59 34
        $reflection = new \ReflectionFunction($callable);
60 34
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
61
    }
62
63
    /**
64
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
65
     *
66
     * This methods allows invoking a constructor and let type hinted parameter names to be
67
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
68
     *
69
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
70
     *
71
     * ```php
72
     * class StringFormatter
73
     * {
74
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
75
     *     {
76
     *         // ...
77
     *     }
78
     * }
79
     *
80
     * $injector = new Yiisoft\Injector\Injector($container);
81
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
82
     * ```
83
     *
84
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
85
     * by the DI container as the second argument.
86
     *
87
     * @param string $class name of the class to be created.
88
     * @param array $arguments The array of the function arguments.
89
     * This can be either a list of arguments, or an associative array where keys are argument names.
90
     * @return mixed object of the given class.
91
     * @throws ContainerExceptionInterface
92
     * @throws MissingRequiredArgumentException|InvalidArgumentException
93
     * @throws ReflectionException
94
     */
95 11
    public function make(string $class, array $arguments = [])
96
    {
97 11
        $classReflection = new ReflectionClass($class);
98 10
        if (!$classReflection->isInstantiable()) {
99 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
100
        }
101 7
        $reflection = $classReflection->getConstructor();
102 7
        if ($reflection === null) {
103
            // Method __construct() does not exist
104 1
            return new $class();
105
        }
106
107 6
        return new $class(...$this->resolveDependencies($reflection, $arguments));
108
    }
109
110
    /**
111
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
112
     * and return array of arguments to call the function with.
113
     *
114
     * @param ReflectionFunctionAbstract $reflection function reflection.
115
     * @param array $arguments concrete arguments.
116
     * @return array resolved arguments.
117
     * @throws ContainerExceptionInterface
118
     * @throws MissingRequiredArgumentException|InvalidArgumentException
119
     * @throws ReflectionException
120
     */
121 40
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
122
    {
123 40
        $this->checkNumericKeyArguments($reflection, $arguments);
124
125 37
        $resolvedArguments = [];
126 37
        $pushUnusedArguments = true;
127 37
        $isInternalOptional = false;
128 37
        $internalParameter = '';
129 37
        foreach ($reflection->getParameters() as $parameter) {
130 33
            if ($isInternalOptional) {
131
                // Check custom parameter definition for an internal function
132
                if (array_key_exists($parameter->getName(), $arguments)) {
133
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
134
                }
135
                continue;
136
            }
137
            // Resolve parameter
138 33
            $resolved = $this->resolveParameter($parameter, $resolvedArguments, $arguments, $pushUnusedArguments);
139 33
            if ($resolved === true) {
140 30
                continue;
141
            }
142
143 5
            if ($resolved === false) {
144 5
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
145
            }
146
            // Internal function. Parameter not resolved
147
            $isInternalOptional = true;
148
            $internalParameter = $parameter->getName();
149
        }
150
151 31
        return $pushUnusedArguments
152 24
            ? [...$resolvedArguments, ...array_filter($arguments, 'is_int', ARRAY_FILTER_USE_KEY)]
153 31
            : $resolvedArguments;
154
    }
155
156
    /**
157
     * @param ReflectionParameter $parameter
158
     * @param array $resolvedArguments
159
     * @param array $arguments
160
     * @param bool $pushUnusedArguments
161
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
162
     * default value in a Reflection object. This is possible for internal functions.
163
     * @throws NotFoundExceptionInterface
164
     * @throws ReflectionException
165
     */
166 33
    private function resolveParameter(
167
        ReflectionParameter $parameter,
168
        array &$resolvedArguments,
169
        array &$arguments,
170
        bool &$pushUnusedArguments
171
    ): ?bool {
172 33
        $name = $parameter->getName();
173 33
        $isVariadic = $parameter->isVariadic();
174 33
        $hasType = $parameter->hasType();
175 33
        $pushUnusedArguments = $pushUnusedArguments && (!$isVariadic || !$hasType);
176
177
        // Get argument by name
178 33
        if (array_key_exists($name, $arguments)) {
179 8
            if ($isVariadic && is_array($arguments[$name])) {
180 1
                $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
181
            } else {
182 7
                $resolvedArguments[] = &$arguments[$name];
183
            }
184 8
            unset($arguments[$name]);
185 8
            return true;
186
        }
187
188 29
        $error = null;
189
190
191 29
        $type = null;
192 29
        $class = null;
193
194 29
        if ($hasType) {
195 28
            $reflectionType = $parameter->getType();
196 28
            $type = $reflectionType->getName();
197 28
            if (!$reflectionType->isBuiltin()) {
198 22
                $class = $type;
199
            }
200
        }
201
202 29
        $isClass = $class !== null || $type === 'object';
203
        try {
204 29
            if ($isClass && $this->resolveObjectParameter($class, $resolvedArguments, $arguments, $isVariadic)) {
205 28
                return true;
206
            }
207 4
        } catch (NotFoundExceptionInterface $e) {
208 4
            $error = $e;
209
        }
210
211 15
        if ($parameter->isDefaultValueAvailable()) {
212 3
            $argument = $parameter->getDefaultValue();
213 3
            $resolvedArguments[] = &$argument;
214 3
            return true;
215
        }
216
217 12
        if (!$parameter->isOptional()) {
218 9
            if ($hasType && $parameter->allowsNull()) {
219 3
                $argument = null;
220 3
                $resolvedArguments[] = &$argument;
221 3
                return true;
222
            }
223
224 6
            if ($error === null) {
225 5
                return false;
226
            }
227
228
            // Throw container exception
229 1
            throw $error;
230
        }
231
232 3
        if ($isVariadic) {
233 3
            return true;
234
        }
235
236
        // Internal function with optional params
237
        $pushUnusedArguments = false;
238
        return null;
239
    }
240
241
    /**
242
     * @param ReflectionFunctionAbstract $reflection
243
     * @param array $arguments
244
     * @throws InvalidArgumentException
245
     */
246 40
    private function checkNumericKeyArguments(ReflectionFunctionAbstract $reflection, array &$arguments): void
247
    {
248 40
        foreach ($arguments as $key => $value) {
249 22
            if (is_int($key) && !is_object($value)) {
250 3
                throw new InvalidArgumentException($reflection, (string)$key);
251
            }
252
        }
253 37
    }
254
255
    /**
256
     * @param null|ReflectionClass $class
257
     * @param array $resolvedArguments
258
     * @param array $arguments
259
     * @param bool $isVariadic
260
     * @return bool True if argument resolved
261
     * @throws ContainerExceptionInterface
262
     */
263 24
    private function resolveObjectParameter(
264
        ?string $class,
265
        array &$resolvedArguments,
266
        array &$arguments,
267
        bool $isVariadic
268
    ): bool {
269 24
        $found = $this->findObjectArguments($class, $resolvedArguments, $arguments, $isVariadic);
270 24
        if ($found || $isVariadic) {
271 12
            return $found;
272
        }
273 17
        if ($class !== null) {
274 16
            $argument = $this->container->get($class);
275 14
            $resolvedArguments[] = &$argument;
276 14
            return true;
277
        }
278 1
        return false;
279
    }
280
281
    /**
282
     * @param null|string $className Null value means objects of any class
283
     * @param array $resolvedArguments
284
     * @param array $arguments
285
     * @param bool $multiple
286
     * @return bool True if arguments are found
287
     */
288 24
    private function findObjectArguments(
289
        ?string $className,
290
        array &$resolvedArguments,
291
        array &$arguments,
292
        bool $multiple
293
    ): bool {
294 24
        $found = false;
295 24
        foreach ($arguments as $key => $item) {
296 11
            if (is_int($key) && is_object($item) && ($className === null || $item instanceof $className)) {
297 10
                $resolvedArguments[] = &$arguments[$key];
298 10
                unset($arguments[$key]);
299 10
                if (!$multiple) {
300 6
                    return true;
301
                }
302 4
                $found = true;
303
            }
304
        }
305 21
        return $found;
306
    }
307
}
308