Passed
Push — master ( 3ad7fe...719238 )
by Alexander
01:23
created

Injector::resolveDependencies()   B

Complexity

Conditions 11
Paths 27

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 11.0069

Importance

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