Passed
Push — master ( ca1e70...42cd18 )
by Aleksei
01:20
created

Injector::findObjectArguments()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 9
c 1
b 0
f 0
nc 4
nop 4
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 7
rs 8.8333
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 2
                if (array_key_exists($parameter->getName(), $arguments)) {
133 1
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
134
                }
135 2
                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
        }
150
151 31
        return $pushUnusedArguments
152 23
            ? [...$resolvedArguments, ...array_filter($arguments, 'is_int', ARRAY_FILTER_USE_KEY)]
153 31
            : $resolvedArguments;
154
    }
155
156
    /**
157
     * @param ReflectionParameter $parameter
158
     * @param array $resolvedArguments
159
     * @param array $arguments
160
     * @param bool $pushUnusedArguments
161
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
162
     * default value in a Reflection object. This is possible for internal functions.
163
     * @throws NotFoundExceptionInterface
164
     * @throws ReflectionException
165
     */
166 34
    private function resolveParameter(
167
        ReflectionParameter $parameter,
168
        array &$resolvedArguments,
169
        array &$arguments,
170
        bool &$pushUnusedArguments
171
    ): ?bool {
172 34
        $name = $parameter->getName();
173 34
        $isVariadic = $parameter->isVariadic();
174 34
        $hasType = $parameter->hasType();
175 34
        $pushUnusedArguments = $pushUnusedArguments && (!$isVariadic || !$hasType);
176
177
        // Get argument by name
178 34
        if (array_key_exists($name, $arguments)) {
179 9
            if ($isVariadic && is_array($arguments[$name])) {
180 1
                $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
181
            } else {
182 8
                $resolvedArguments[] = &$arguments[$name];
183
            }
184 9
            unset($arguments[$name]);
185 9
            return true;
186
        }
187
188 30
        $error = null;
189 30
        $class = $parameter->getClass();
190 30
        $type = $hasType ? $parameter->getType()->getName() : null;
191 30
        $isClass = $class !== null || $type === 'object';
192
        try {
193 30
            if ($isClass && $this->resolveObjectParameter($class, $resolvedArguments, $arguments, $isVariadic)) {
194 29
                return true;
195
            }
196 3
        } catch (NotFoundExceptionInterface $e) {
197 3
            $error = $e;
198
        }
199
200 16
        if ($parameter->isDefaultValueAvailable()) {
201 2
            $argument = $parameter->getDefaultValue();
202 2
            $resolvedArguments[] = &$argument;
203 2
            return true;
204
        }
205
206 14
        if (!$parameter->isOptional()) {
207 9
            if ($hasType && $parameter->allowsNull()) {
208 3
                $argument = null;
209 3
                $resolvedArguments[] = &$argument;
210 3
                return true;
211
            }
212
213 6
            if ($error === null) {
214 5
                return false;
215
            }
216
217
            // Throw container exception
218 1
            throw $error;
219
        }
220
221 5
        if ($isVariadic) {
222 3
            return true;
223
        }
224
225
        // Internal function with optional params
226 2
        $pushUnusedArguments = false;
227 2
        return null;
228
    }
229
230
    /**
231
     * @param ReflectionFunctionAbstract $reflection
232
     * @param array $arguments
233
     * @throws InvalidArgumentException
234
     */
235 41
    private function checkNumericKeyArguments(ReflectionFunctionAbstract $reflection, array &$arguments): void
236
    {
237 41
        foreach ($arguments as $key => $value) {
238 23
            if (is_int($key) && !is_object($value)) {
239 3
                throw new InvalidArgumentException($reflection, (string)$key);
240
            }
241
        }
242
    }
243
244
    /**
245
     * @param null|ReflectionClass $class
246
     * @param array $resolvedArguments
247
     * @param array $arguments
248
     * @param bool $isVariadic
249
     * @return bool True if argument resolved
250
     * @throws ContainerExceptionInterface
251
     */
252 23
    private function resolveObjectParameter(
253
        ?ReflectionClass $class,
254
        array &$resolvedArguments,
255
        array &$arguments,
256
        bool $isVariadic
257
    ): bool {
258 23
        $className = $class !== null ? $class->getName() : null;
259 23
        $found = $this->findObjectArguments($className, $resolvedArguments, $arguments, $isVariadic);
260 23
        if ($found || $isVariadic) {
261 12
            return $found;
262
        }
263 16
        if ($className !== null) {
264 15
            $argument = $this->container->get($className);
265 14
            $resolvedArguments[] = &$argument;
266 14
            return true;
267
        }
268 1
        return false;
269
    }
270
271
    /**
272
     * @param null|string $className Null value means objects of any class
273
     * @param array $resolvedArguments
274
     * @param array $arguments
275
     * @param bool $multiple
276
     * @return bool True if arguments are found
277
     */
278 23
    private function findObjectArguments(
279
        ?string $className,
280
        array &$resolvedArguments,
281
        array &$arguments,
282
        bool $multiple
283
    ): bool {
284 23
        $found = false;
285 23
        foreach ($arguments as $key => $item) {
286 11
            if (is_int($key) && is_object($item) && ($className === null || $item instanceof $className)) {
287 10
                $resolvedArguments[] = &$arguments[$key];
288 10
                unset($arguments[$key]);
289 10
                if (!$multiple) {
290 6
                    return true;
291
                }
292 4
                $found = true;
293
            }
294
        }
295 20
        return $found;
296
    }
297
}
298