Passed
Pull Request — master (#6)
by
unknown
01:33
created

Injector   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 98.63%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 75
dl 0
loc 196
ccs 72
cts 73
cp 0.9863
rs 9.6
c 8
b 0
f 1
wmc 35

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A invoke() 0 13 4
A make() 0 13 3
F resolveDependencies() 0 86 27
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
12
/**
13
 * Injector is able to analyze callable dependencies based on
14
 * type hinting and inject them from any PSR-11 compatible container.
15
 */
16
final class Injector
17
{
18
    private ContainerInterface $container;
19
20 44
    public function __construct(ContainerInterface $container)
21
    {
22 44
        $this->container = $container;
23
    }
24
25
    /**
26
     * Invoke a callback with resolving dependencies based on parameter types.
27
     *
28
     * This methods allows invoking a callback and let type hinted parameter names to be
29
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
30
     *
31
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
32
     *
33
     * ```php
34
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
35
     *    // ...
36
     * }
37
     *
38
     * $injector = new Yiisoft\Injector\Injector($container);
39
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
40
     * ```
41
     *
42
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
43
     * by the DI container as the second argument.
44
     *
45
     * @param callable $callable callable to be invoked.
46
     * @param array $arguments The array of the function arguments.
47
     * This can be either a list of arguments, or an associative array where keys are argument names.
48
     * @return mixed the callable return value.
49
     * @throws MissingRequiredArgumentException if required argument is missing.
50
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
51
     * @throws ReflectionException
52
     */
53 33
    public function invoke(callable $callable, array $arguments = [])
54
    {
55 33
        if (\is_object($callable) && !$callable instanceof \Closure) {
56
            $callable = [$callable, '__invoke'];
57
        }
58
59 33
        if (\is_array($callable)) {
60 3
            $reflection = new \ReflectionMethod($callable[0], $callable[1]);
61
        } else {
62 30
            $reflection = new \ReflectionFunction($callable);
63
        }
64
65 33
        return \call_user_func_array($callable, $this->resolveDependencies($reflection, $arguments));
66
    }
67
68
    /**
69
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
70
     *
71
     * This methods allows invoking a constructor and let type hinted parameter names to be
72
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
73
     *
74
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
75
     *
76
     * ```php
77
     * class StringFormatter
78
     * {
79
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
80
     *     {
81
     *         // ...
82
     *     }
83
     * }
84
     *
85
     * $injector = new Yiisoft\Injector\Injector($container);
86
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
87
     * ```
88
     *
89
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
90
     * by the DI container as the second argument.
91
     *
92
     * @param string $class name of the class to be created.
93
     * @param array $arguments The array of the function arguments.
94
     * This can be either a list of arguments, or an associative array where keys are argument names.
95
     * @return mixed object of the given class.
96
     * @throws ContainerExceptionInterface
97
     * @throws MissingRequiredArgumentException|InvalidArgumentException
98
     * @throws ReflectionException
99
     */
100 11
    public function make(string $class, array $arguments = [])
101
    {
102 11
        $classReflection = new \ReflectionClass($class);
103 10
        if (!$classReflection->isInstantiable()) {
104 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
105
        }
106 7
        $reflection = $classReflection->getConstructor();
107 7
        if ($reflection === null) {
108
            // Method __construct() does not exist
109 1
            return new $class();
110
        }
111
112 6
        return new $class(...$this->resolveDependencies($reflection, $arguments));
113
    }
114
115
    /**
116
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
117
     * and return array of arguments to call the function with.
118
     *
119
     * @param \ReflectionFunctionAbstract $reflection function reflection.
120
     * @param array $arguments concrete arguments.
121
     * @return array resolved arguments.
122
     * @throws ContainerExceptionInterface
123
     * @throws MissingRequiredArgumentException|InvalidArgumentException
124
     * @throws ReflectionException
125
     */
126 39
    private function resolveDependencies(\ReflectionFunctionAbstract $reflection, array $arguments = []): array
127
    {
128 39
        $resolvedArguments = [];
129
130 39
        $pushUnusedArguments = true;
131 39
        foreach ($reflection->getParameters() as $parameter) {
132 35
            $name = $parameter->getName();
133 35
            $class = $parameter->getClass();
134 35
            $hasType = $parameter->hasType();
135 35
            $isNullable = $parameter->allowsNull() && $hasType;
136 35
            $isVariadic = $parameter->isVariadic();
137 35
            $error = null;
138 35
            unset($tmpValue);
139
140
            // Get argument by name
141 35
            if (array_key_exists($name, $arguments)) {
142 8
                if ($isVariadic && is_array($arguments[$name])) {
143 1
                    $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
144
                } else {
145 7
                    $resolvedArguments[] = &$arguments[$name];
146
                }
147 8
                unset($arguments[$name]);
148 8
                continue;
149
            }
150
151 31
            $type = $hasType ? $parameter->getType()->getName() : null;
152 31
            if ($class !== null || $type === 'object') {
153
                // Unnamed arguments
154 23
                $className = $class !== null ? $class->getName() : null;
155 23
                $found = false;
156 23
                foreach ($arguments as $key => $item) {
157 11
                    if (!is_int($key)) {
158 1
                        continue;
159
                    }
160 10
                    if (is_object($item) and $className === null || $item instanceof $className) {
161 9
                        $found = true;
162 9
                        $resolvedArguments[] = &$arguments[$key];
163 9
                        unset($arguments[$key], $item);
164 9
                        if (!$isVariadic) {
165 5
                            break;
166
                        }
167
                    }
168
                }
169 23
                if ($found) {
170 9
                    $pushUnusedArguments = false;
171 9
                    continue;
172
                }
173
174 19
                if ($className !== null) {
175
                    // If the argument is optional we catch not instantiable exceptions
176
                    try {
177 18
                        $tmpValue = $this->container->get($className);
178 16
                        $resolvedArguments[] = &$tmpValue;
179 16
                        continue;
180 4
                    } catch (NotFoundExceptionInterface $e) {
181 4
                        $error = $e;
182
                    }
183
                }
184
            }
185
186 16
            if ($parameter->isDefaultValueAvailable()) {
187 2
                $tmpValue = $parameter->getDefaultValue();
188 2
                $resolvedArguments[] = &$tmpValue;
189 14
            } elseif (!$parameter->isOptional()) {
190 10
                if ($isNullable) {
191 3
                    $tmpValue = null;
192 3
                    $resolvedArguments[] = &$tmpValue;
193
                } else {
194 10
                    throw $error ?? new MissingRequiredArgumentException($name, $reflection->getName());
195
                }
196 4
            } elseif ($hasType) {
197 2
                $pushUnusedArguments = false;
198
            }
199
        }
200
201 32
        foreach ($arguments as $key => $value) {
202 7
            if (is_int($key)) {
203 7
                if (!is_object($value)) {
204 2
                    throw new InvalidArgumentException((string)$key, $reflection->getName());
205
                }
206 5
                if ($pushUnusedArguments) {
207 2
                    $resolvedArguments[] = $value;
208
                }
209
            }
210
        }
211 30
        return $resolvedArguments;
212
    }
213
}
214