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

Injector   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 98.9%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 90
c 8
b 0
f 1
dl 0
loc 244
ccs 90
cts 91
cp 0.989
rs 9.2
wmc 40

5 Methods

Rating   Name   Duplication   Size   Complexity  
A invoke() 0 5 1
A make() 0 13 3
A __construct() 0 3 1
D resolveParameter() 0 88 24
B resolveDependencies() 0 42 11

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 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