Passed
Pull Request — master (#7)
by Alexander
02:41 queued 01:11
created

Injector::resolveDependencies()   F

Complexity

Conditions 27
Paths 2765

Size

Total Lines 86
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 27

Importance

Changes 0
Metric Value
cc 27
eloc 58
nc 2765
nop 2
dl 0
loc 86
ccs 56
cts 56
cp 1
crap 27
rs 0
c 0
b 0
f 0

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 ReflectionException;
11
12
/**
13
 * Injector is able to analyze callable dependencies based on
14
 * type hinting and inject them from any PSR-11 compatible container.
15
 */
16
final class Injector
17
{
18
    private ContainerInterface $container;
19
20 45
    public function __construct(ContainerInterface $container)
21
    {
22 45
        $this->container = $container;
23
    }
24
25
    /**
26
     * Invoke a callback with resolving dependencies based on parameter types.
27
     *
28
     * This methods allows invoking a callback and let type hinted parameter names to be
29
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
30
     *
31
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
32
     *
33
     * ```php
34
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
35
     *    // ...
36
     * }
37
     *
38
     * $injector = new Yiisoft\Injector\Injector($container);
39
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
40
     * ```
41
     *
42
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
43
     * by the DI container as the second argument.
44
     *
45
     * @param callable $callable callable to be invoked.
46
     * @param array $arguments The array of the function arguments.
47
     * This can be either a list of arguments, or an associative array where keys are argument names.
48
     * @return mixed the callable return value.
49
     * @throws MissingRequiredArgumentException if required argument is missing.
50
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
51
     * @throws ReflectionException
52
     */
53 34
    public function invoke(callable $callable, array $arguments = [])
54
    {
55 34
        $callable = \Closure::fromCallable($callable);
56 34
        $reflection = new \ReflectionFunction($callable);
57 34
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
58
    }
59
60
    /**
61
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
62
     *
63
     * This methods allows invoking a constructor and let type hinted parameter names to be
64
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
65
     *
66
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
67
     *
68
     * ```php
69
     * class StringFormatter
70
     * {
71
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
72
     *     {
73
     *         // ...
74
     *     }
75
     * }
76
     *
77
     * $injector = new Yiisoft\Injector\Injector($container);
78
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
79
     * ```
80
     *
81
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
82
     * by the DI container as the second argument.
83
     *
84
     * @param string $class name of the class to be created.
85
     * @param array $arguments The array of the function arguments.
86
     * This can be either a list of arguments, or an associative array where keys are argument names.
87
     * @return mixed object of the given class.
88
     * @throws ContainerExceptionInterface
89
     * @throws MissingRequiredArgumentException|InvalidArgumentException
90
     * @throws ReflectionException
91
     */
92 11
    public function make(string $class, array $arguments = [])
93
    {
94 11
        $classReflection = new \ReflectionClass($class);
95 10
        if (!$classReflection->isInstantiable()) {
96 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
97
        }
98 7
        $reflection = $classReflection->getConstructor();
99 7
        if ($reflection === null) {
100
            // Method __construct() does not exist
101 1
            return new $class();
102
        }
103
104 6
        return new $class(...$this->resolveDependencies($reflection, $arguments));
105
    }
106
107
    /**
108
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
109
     * and return array of arguments to call the function with.
110
     *
111
     * @param \ReflectionFunctionAbstract $reflection function reflection.
112
     * @param array $arguments concrete arguments.
113
     * @return array resolved arguments.
114
     * @throws ContainerExceptionInterface
115
     * @throws MissingRequiredArgumentException|InvalidArgumentException
116
     * @throws ReflectionException
117
     */
118 40
    private function resolveDependencies(\ReflectionFunctionAbstract $reflection, array $arguments = []): array
119
    {
120 40
        $resolvedArguments = [];
121
122 40
        $pushUnusedArguments = true;
123 40
        foreach ($reflection->getParameters() as $parameter) {
124 35
            $name = $parameter->getName();
125 35
            $class = $parameter->getClass();
126 35
            $hasType = $parameter->hasType();
127 35
            $isNullable = $parameter->allowsNull() && $hasType;
128 35
            $isVariadic = $parameter->isVariadic();
129 35
            $error = null;
130 35
            unset($tmpValue);
131
132
            // Get argument by name
133 35
            if (array_key_exists($name, $arguments)) {
134 8
                if ($isVariadic && is_array($arguments[$name])) {
135 1
                    $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
136
                } else {
137 7
                    $resolvedArguments[] = &$arguments[$name];
138
                }
139 8
                unset($arguments[$name]);
140 8
                continue;
141
            }
142
143 31
            $type = $hasType ? $parameter->getType()->getName() : null;
144 31
            if ($class !== null || $type === 'object') {
145
                // Unnamed arguments
146 23
                $className = $class !== null ? $class->getName() : null;
147 23
                $found = false;
148 23
                foreach ($arguments as $key => $item) {
149 11
                    if (!is_int($key)) {
150 1
                        continue;
151
                    }
152 10
                    if (is_object($item) and $className === null || $item instanceof $className) {
153 9
                        $found = true;
154 9
                        $resolvedArguments[] = &$arguments[$key];
155 9
                        unset($arguments[$key], $item);
156 9
                        if (!$isVariadic) {
157 5
                            break;
158
                        }
159
                    }
160
                }
161 23
                if ($found) {
162 9
                    $pushUnusedArguments = false;
163 9
                    continue;
164
                }
165
166 19
                if ($className !== null) {
167
                    // If the argument is optional we catch not instantiable exceptions
168
                    try {
169 18
                        $tmpValue = $this->container->get($className);
170 16
                        $resolvedArguments[] = &$tmpValue;
171 16
                        continue;
172 4
                    } catch (NotFoundExceptionInterface $e) {
173 4
                        $error = $e;
174
                    }
175
                }
176
            }
177
178 16
            if ($parameter->isDefaultValueAvailable()) {
179 2
                $tmpValue = $parameter->getDefaultValue();
180 2
                $resolvedArguments[] = &$tmpValue;
181 14
            } elseif (!$parameter->isOptional()) {
182 10
                if ($isNullable) {
183 3
                    $tmpValue = null;
184 3
                    $resolvedArguments[] = &$tmpValue;
185
                } else {
186 10
                    throw $error ?? new MissingRequiredArgumentException($name, $reflection->getName());
187
                }
188 4
            } elseif ($hasType) {
189 2
                $pushUnusedArguments = false;
190
            }
191
        }
192
193 33
        foreach ($arguments as $key => $value) {
194 7
            if (is_int($key)) {
195 7
                if (!is_object($value)) {
196 2
                    throw new InvalidArgumentException((string)$key, $reflection->getName());
197
                }
198 5
                if ($pushUnusedArguments) {
199 2
                    $resolvedArguments[] = $value;
200
                }
201
            }
202
        }
203 31
        return $resolvedArguments;
204
    }
205
}
206