Passed
Pull Request — master (#10)
by
unknown
01:22
created

Injector   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Test Coverage

Coverage 98.9%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 91
c 8
b 0
f 1
dl 0
loc 235
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 81 24
B resolveDependencies() 0 40 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 8
            } elseif ($resolved === false) {
138 6
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
139
            }
140
            // Internal function. Parameter not resolved
141 2
            $isInternalOptional = true;
142 2
            $internalParameter = $parameter->getName();
143 2
            if (count($arguments) === 0) {
144 1
                break;
145
            }
146
        }
147
148 33
        foreach ($arguments as $key => $value) {
149 7
            if (is_int($key)) {
150 7
                if (!is_object($value)) {
151 2
                    throw new InvalidArgumentException($reflection, (string)$key);
152
                }
153 5
                if ($pushUnusedArguments) {
154 4
                    $resolvedArguments[] = $value;
155
                }
156
            }
157
        }
158 31
        return $resolvedArguments;
159
    }
160
161
    /**
162
     * @param ReflectionParameter $parameter
163
     * @param array $resolvedArguments
164
     * @param array $arguments
165
     * @param bool $pushUnusedArguments
166
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
167
     * default value in a Reflection object. This is possible for internal functions.
168
     * @throws NotFoundExceptionInterface
169
     * @throws ReflectionException
170
     */
171 36
    private function resolveParameter(
172
        ReflectionParameter $parameter,
173
        array &$resolvedArguments,
174
        array &$arguments,
175
        bool &$pushUnusedArguments
176
    ): ?bool {
177 36
        $name = $parameter->getName();
178 36
        $isVariadic = $parameter->isVariadic();
179
180
        // Get argument by name
181 36
        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 32
        $class = $parameter->getClass();
192 32
        $hasType = $parameter->hasType();
193 32
        $type = $hasType ? $parameter->getType()->getName() : null;
194 32
        $error = null;
195 32
        if ($class !== null || $type === 'object') {
196
            // Unnamed arguments
197 23
            $className = $class !== null ? $class->getName() : null;
198 23
            $found = false;
199 23
            foreach ($arguments as $key => $item) {
200 11
                if (!is_int($key)) {
201 1
                    continue;
202
                }
203 10
                if (is_object($item) and $className === null || $item instanceof $className) {
204 10
                    $found = true;
205 10
                    $resolvedArguments[] = &$arguments[$key];
206 10
                    unset($arguments[$key]);
207 10
                    if (!$isVariadic) {
208 6
                        return true;
209
                    }
210
                }
211
            }
212 20
            if ($found) {
213
                // Only for variadic arguments
214 4
                $pushUnusedArguments = false;
215 4
                return true;
216
            }
217
218 18
            if ($className !== null) {
219
                // If the argument is optional we catch not instantiable exceptions
220
                try {
221 17
                    $argument = $this->container->get($className);
222 15
                    $resolvedArguments[] = &$argument;
223 15
                    return true;
224 4
                } catch (NotFoundExceptionInterface $e) {
225 4
                    $error = $e;
226
                }
227
            }
228
        }
229
230 17
        if ($parameter->isDefaultValueAvailable()) {
231 2
            $argument = $parameter->getDefaultValue();
232 2
            $resolvedArguments[] = &$argument;
233 2
            return true;
234 15
        } elseif (!$parameter->isOptional()) {
235 10
            if ($parameter->allowsNull() && $hasType) {
236 3
                $argument = null;
237 3
                $resolvedArguments[] = &$argument;
238 3
                return true;
239 7
            } elseif ($error === null) {
240 6
                return false;
241
            } else {
242
                // Throw container exception
243 1
                throw $error;
244
            }
245 5
        } elseif ($isVariadic) {
246 3
            $pushUnusedArguments = !$hasType && $pushUnusedArguments;
247 3
            return true;
248
        }
249
        // Internal function with optional params
250 2
        $pushUnusedArguments = false;
251 2
        return null;
252
    }
253
}
254