Passed
Push — master ( b71587...3ad7fe )
by Alexander
01:34
created

Injector::resolveParameter()   D

Complexity

Conditions 23
Paths 96

Size

Total Lines 75
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 23

Importance

Changes 0
Metric Value
cc 23
eloc 49
nc 96
nop 4
dl 0
loc 75
ccs 47
cts 47
cp 1
crap 23
rs 4.1666
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
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 45
    public function __construct(ContainerInterface $container)
22
    {
23 45
        $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 11
    public function make(string $class, array $arguments = [])
94
    {
95 11
        $classReflection = new \ReflectionClass($class);
96 10
        if (!$classReflection->isInstantiable()) {
97 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
98
        }
99 7
        $reflection = $classReflection->getConstructor();
100 7
        if ($reflection === null) {
101
            // Method __construct() does not exist
102 1
            return new $class();
103
        }
104
105 6
        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 40
    private function resolveDependencies(\ReflectionFunctionAbstract $reflection, array $arguments = []): array
120
    {
121 40
        $resolvedArguments = [];
122
123 40
        $pushUnusedArguments = true;
124 40
        foreach ($reflection->getParameters() as $parameter) {
125 35
            if (!$this->resolveParameter($parameter, $resolvedArguments, $arguments, $pushUnusedArguments)) {
126 6
                throw new MissingRequiredArgumentException($parameter->getName(), $reflection->getName());
127
            }
128
        }
129
130 33
        foreach ($arguments as $key => $value) {
131 7
            if (is_int($key)) {
132 7
                if (!is_object($value)) {
133 2
                    throw new InvalidArgumentException((string)$key, $reflection->getName());
134
                }
135 5
                if ($pushUnusedArguments) {
136 4
                    $resolvedArguments[] = $value;
137
                }
138
            }
139
        }
140 31
        return $resolvedArguments;
141
    }
142
143
    /**
144
     * @param ReflectionParameter $parameter
145
     * @param array $resolvedArguments
146
     * @param array $arguments
147
     * @param bool $pushUnusedArguments
148
     * @return bool
149
     * @throws NotFoundExceptionInterface
150
     * @throws ReflectionException
151
     */
152 35
    private function resolveParameter(
153
        ReflectionParameter $parameter,
154
        array &$resolvedArguments,
155
        array &$arguments,
156
        bool &$pushUnusedArguments
157
    ): bool {
158 35
        $name = $parameter->getName();
159 35
        $isVariadic = $parameter->isVariadic();
160
161
        // Get argument by name
162 35
        if (array_key_exists($name, $arguments)) {
163 8
            if ($isVariadic && is_array($arguments[$name])) {
164 1
                $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
165
            } else {
166 7
                $resolvedArguments[] = &$arguments[$name];
167
            }
168 8
            unset($arguments[$name]);
169 8
            return true;
170
        }
171
172 31
        $class = $parameter->getClass();
173 31
        $hasType = $parameter->hasType();
174 31
        $type = $hasType ? $parameter->getType()->getName() : null;
175 31
        $error = null;
176 31
        if ($class !== null || $type === 'object') {
177
            // Unnamed arguments
178 23
            $className = $class !== null ? $class->getName() : null;
179 23
            $found = false;
180 23
            foreach ($arguments as $key => $item) {
181 11
                if (!is_int($key)) {
182 1
                    continue;
183
                }
184 10
                if (is_object($item) and $className === null || $item instanceof $className) {
185 10
                    $found = true;
186 10
                    $resolvedArguments[] = &$arguments[$key];
187 10
                    unset($arguments[$key]);
188 10
                    if (!$isVariadic) {
189 6
                        return true;
190
                    }
191
                }
192
            }
193 20
            if ($found) {
194
                // Only for variadic arguments
195 4
                $pushUnusedArguments = false;
196 4
                return true;
197
            }
198
199 18
            if ($className !== null) {
200
                // If the argument is optional we catch not instantiable exceptions
201
                try {
202 17
                    $argument = $this->container->get($className);
203 15
                    $resolvedArguments[] = &$argument;
204 15
                    return true;
205 4
                } catch (NotFoundExceptionInterface $e) {
206 4
                    $error = $e;
207
                }
208
            }
209
        }
210
211 16
        if ($parameter->isDefaultValueAvailable()) {
212 2
            $argument = $parameter->getDefaultValue();
213 2
            $resolvedArguments[] = &$argument;
214 14
        } elseif (!$parameter->isOptional()) {
215 10
            if ($parameter->allowsNull() && $hasType) {
216 3
                $argument = null;
217 3
                $resolvedArguments[] = &$argument;
218 7
            } elseif ($error === null) {
219 6
                return false;
220
            } else {
221 4
                throw $error;
222
            }
223 4
        } elseif ($hasType) {
224 2
            $pushUnusedArguments = false;
225
        }
226 9
        return true;
227
    }
228
}
229