Passed
Push — master ( 57b861...ca1e70 )
by Alexander
01:33
created

Injector   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 265
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 9
Bugs 0 Features 1
Metric Value
eloc 90
dl 0
loc 265
ccs 93
cts 93
cp 1
rs 8.8798
c 9
b 0
f 1
wmc 44

7 Methods

Rating   Name   Duplication   Size   Complexity  
A checkNumericKeyArguments() 0 5 4
B resolveDependencies() 0 36 8
A invoke() 0 5 1
A make() 0 13 3
A __construct() 0 3 1
B resolveObjectParameter() 0 29 10
D resolveParameter() 0 62 17

How to fix   Complexity   

Complex Class

Complex classes like Injector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Injector, and based on these observations, apply Extract Interface, too.

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 46
    public function __construct(ContainerInterface $container)
24
    {
25 46
        $this->container = $container;
26
    }
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 12
    public function make(string $class, array $arguments = [])
96
    {
97 12
        $classReflection = new ReflectionClass($class);
98 11
        if (!$classReflection->isInstantiable()) {
99 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
100
        }
101 8
        $reflection = $classReflection->getConstructor();
102 8
        if ($reflection === null) {
103
            // Method __construct() does not exist
104 1
            return new $class();
105
        }
106
107 7
        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 41
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
122
    {
123 41
        $this->checkNumericKeyArguments($reflection, $arguments);
124
125 38
        $resolvedArguments = [];
126 38
        $pushUnusedArguments = true;
127 38
        $isInternalOptional = false;
128 38
        $internalParameter = '';
129 38
        foreach ($reflection->getParameters() as $parameter) {
130 34
            if ($isInternalOptional) {
131
                // Check custom parameter definition for an internal function
132 1
                if (array_key_exists($parameter->getName(), $arguments)) {
133 1
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
134
                }
135 1
                continue;
136
            }
137
            // Resolve parameter
138 34
            $resolved = $this->resolveParameter($parameter, $resolvedArguments, $arguments, $pushUnusedArguments);
139 34
            if ($resolved === true) {
140 30
                continue;
141
            }
142
143 7
            if ($resolved === false) {
144 5
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
145
            }
146
            // Internal function. Parameter not resolved
147 2
            $isInternalOptional = true;
148 2
            $internalParameter = $parameter->getName();
149 2
            if (count($arguments) === 0) {
150 1
                break;
151
            }
152
        }
153
154 31
        return $pushUnusedArguments
155 23
            ? [...$resolvedArguments, ...array_filter($arguments, 'is_int', ARRAY_FILTER_USE_KEY)]
156 31
            : $resolvedArguments;
157
    }
158
159
    /**
160
     * @param ReflectionParameter $parameter
161
     * @param array $resolvedArguments
162
     * @param array $arguments
163
     * @param bool $pushUnusedArguments
164
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
165
     * default value in a Reflection object. This is possible for internal functions.
166
     * @throws NotFoundExceptionInterface
167
     * @throws ReflectionException
168
     */
169 34
    private function resolveParameter(
170
        ReflectionParameter $parameter,
171
        array &$resolvedArguments,
172
        array &$arguments,
173
        bool &$pushUnusedArguments
174
    ): ?bool {
175 34
        $name = $parameter->getName();
176 34
        $isVariadic = $parameter->isVariadic();
177 34
        $hasType = $parameter->hasType();
178 34
        $pushUnusedArguments = $pushUnusedArguments && (!$isVariadic || !$hasType);
179
180
        // Get argument by name
181 34
        if (array_key_exists($name, $arguments)) {
182 9
            if ($isVariadic && is_array($arguments[$name])) {
183 1
                $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
184
            } else {
185 8
                $resolvedArguments[] = &$arguments[$name];
186
            }
187 9
            unset($arguments[$name]);
188 9
            return true;
189
        }
190
191 30
        $error = null;
192 30
        $class = $parameter->getClass();
193 30
        $type = $hasType ? $parameter->getType()->getName() : null;
194 30
        $isClass = $class !== null || $type === 'object';
195
        try {
196 30
            if ($isClass && $this->resolveObjectParameter($class, $resolvedArguments, $arguments, $isVariadic)) {
197 29
                return true;
198
            }
199 3
        } catch (NotFoundExceptionInterface $e) {
200 3
            $error = $e;
201
        }
202
203 16
        if ($parameter->isDefaultValueAvailable()) {
204 2
            $argument = $parameter->getDefaultValue();
205 2
            $resolvedArguments[] = &$argument;
206 2
            return true;
207
        }
208
209 14
        if (!$parameter->isOptional()) {
210 9
            if ($hasType && $parameter->allowsNull()) {
211 3
                $argument = null;
212 3
                $resolvedArguments[] = &$argument;
213 3
                return true;
214
            }
215
216 6
            if ($error === null) {
217 5
                return false;
218
            }
219
220
            // Throw container exception
221 1
            throw $error;
222
        }
223
224 5
        if ($isVariadic) {
225 3
            return true;
226
        }
227
228
        // Internal function with optional params
229 2
        $pushUnusedArguments = false;
230 2
        return null;
231
    }
232
233
    /**
234
     * @param ReflectionFunctionAbstract $reflection
235
     * @param array $arguments
236
     * @throws InvalidArgumentException
237
     */
238 41
    private function checkNumericKeyArguments(ReflectionFunctionAbstract $reflection, array &$arguments): void
239
    {
240 41
        foreach ($arguments as $key => $value) {
241 23
            if (is_int($key) && !is_object($value)) {
242 3
                throw new InvalidArgumentException($reflection, (string)$key);
243
            }
244
        }
245
    }
246
247
    /**
248
     * @param null|ReflectionClass $class
249
     * @param array $resolvedArguments
250
     * @param array $arguments
251
     * @param bool $isVariadic
252
     * @return bool True if argument resolved
253
     * @throws ContainerExceptionInterface
254
     */
255 23
    private function resolveObjectParameter(
256
        ?ReflectionClass $class,
257
        array &$resolvedArguments,
258
        array &$arguments,
259
        bool $isVariadic
260
    ): bool {
261
        // Unnamed arguments
262 23
        $className = $class !== null ? $class->getName() : null;
263 23
        $found = false;
264 23
        foreach ($arguments as $key => $item) {
265 11
            if (is_int($key) && is_object($item) && ($className === null || $item instanceof $className)) {
266 10
                $resolvedArguments[] = &$arguments[$key];
267 10
                unset($arguments[$key]);
268 10
                if (!$isVariadic) {
269 6
                    return true;
270
                }
271 4
                $found = true;
272
            }
273
        }
274 20
        if ($isVariadic) {
275 6
            return $found;
276
        }
277
278 16
        if ($className !== null) {
279 15
            $argument = $this->container->get($className);
280 14
            $resolvedArguments[] = &$argument;
281 14
            return true;
282
        }
283 1
        return false;
284
    }
285
}
286