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

Injector::resolveDependencies()   B

Complexity

Conditions 11
Paths 27

Size

Total Lines 40
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 40
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 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