Passed
Pull Request — master (#4)
by
unknown
01:13
created

Injector   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 160
Duplicated Lines 0 %

Test Coverage

Coverage 98.51%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 69
dl 0
loc 160
ccs 66
cts 67
cp 0.9851
rs 10
c 8
b 0
f 1
wmc 29

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
F resolveDependencies() 0 79 21
A invoke() 0 13 4
A make() 0 13 3
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 39
    public function __construct(ContainerInterface $container)
21
    {
22 39
        $this->container = $container;
23
    }
24
25
    /**
26
     * Invoke a callback with resolving dependencies in parameters.
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
     * $container->invoke($formatString, ['string' => 'Hello World!']);
38
     * ```
39
     *
40
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
41
     * by the DI container as the second argument.
42
     *
43
     * @param callable $callable callable to be invoked.
44
     * @param array $arguments The array of the function arguments.
45
     * This can be either a list of arguments, or an associative array where keys are argument names.
46
     * @return mixed the callable return value.
47
     * @throws MissingRequiredArgumentException  if required argument is missing.
48
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
49
     * @throws ReflectionException
50
     */
51 28
    public function invoke(callable $callable, array $arguments = [])
52
    {
53 28
        if (\is_object($callable) && !$callable instanceof \Closure) {
54
            $callable = [$callable, '__invoke'];
55
        }
56
57 28
        if (\is_array($callable)) {
58 3
            $reflection = new \ReflectionMethod($callable[0], $callable[1]);
59
        } else {
60 25
            $reflection = new \ReflectionFunction($callable);
61
        }
62
63 28
        return \call_user_func_array($callable, $this->resolveDependencies($reflection, $arguments));
64
    }
65
66
    /**
67
     * @param string $class
68
     * @param array $arguments
69
     * @return mixed
70
     * @throws ContainerExceptionInterface
71
     * @throws MissingRequiredArgumentException|InvalidArgumentException
72
     * @throws ReflectionException
73
     */
74 11
    public function make(string $class, array $arguments = [])
75
    {
76 11
        $classReflection = new \ReflectionClass($class);
77 10
        if (!$classReflection->isInstantiable()) {
78 3
            throw new \InvalidArgumentException("Class $class is not instantiable");
79
        }
80 7
        $reflection = $classReflection->getConstructor();
81 7
        if ($reflection === null) {
82
            // Method __construct() does not exist
83 1
            return new $class;
84
        }
85
86 6
        return new $class(...$this->resolveDependencies($reflection, $arguments));
87
    }
88
89
    /**
90
     * @param \ReflectionFunctionAbstract $reflection
91
     * @param array $arguments
92
     * @return array
93
     * @throws ContainerExceptionInterface
94
     * @throws MissingRequiredArgumentException|InvalidArgumentException
95
     * @throws ReflectionException
96
     */
97 34
    private function resolveDependencies(\ReflectionFunctionAbstract $reflection, array $arguments = [])
98
    {
99 34
        $resolvedArguments = [];
100
101 34
        $pushUnusedArguments = true;
102 34
        foreach ($reflection->getParameters() as $parameter) {
103 30
            $name = $parameter->getName();
104 30
            $class = $parameter->getClass();
105 30
            $hasType = $parameter->hasType();
106 30
            $isNullable = $parameter->allowsNull() && $hasType;
107 30
            $isVariadic = $parameter->isVariadic();
108 30
            $error = null;
109
110
            // Get argument by name
111 30
            if (array_key_exists($name, $arguments)) {
112 6
                if ($isVariadic && is_array($arguments[$name])) {
113 1
                    $resolvedArguments = array_merge($resolvedArguments, array_values($arguments[$name]));
114
                } else {
115 5
                    $resolvedArguments[] = $arguments[$name];
116
                }
117 6
                unset($arguments[$name]);
118 6
                continue;
119
            }
120
121 27
            if ($class !== null) {
122
                // Unnamed arguments
123 19
                $className = $class->getName();
124 19
                $found = false;
125 19
                foreach ($arguments as $key => $item) {
126 8
                    if (!is_int($key)) {
127 1
                        continue;
128
                    }
129 7
                    if ($item instanceof $className) {
130 6
                        $found = true;
131 6
                        $resolvedArguments[] = $item;
132 6
                        unset($arguments[$key]);
133 6
                        if (!$isVariadic) {
134 4
                            break;
135
                        }
136
                    }
137
                }
138 19
                if ($found) {
139 6
                    $pushUnusedArguments = false;
140 6
                    continue;
141
                }
142
143
                // If the argument is optional we catch not instantiable exceptions
144
                try {
145 17
                    $resolvedArguments[] = $this->container->get($className);
146 15
                    continue;
147 3
                } catch (NotFoundExceptionInterface $e) {
148 3
                    $error = $e;
149
                }
150
            }
151
152 14
            if ($parameter->isDefaultValueAvailable()) {
153 2
                $resolvedArguments[] = $parameter->getDefaultValue();
154 12
            } elseif (!$parameter->isOptional()) {
155 8
                if ($isNullable) {
156 2
                    $resolvedArguments[] = null;
157
                } else {
158 8
                    throw $error ?? new MissingRequiredArgumentException($name, $reflection->getName());
159
                }
160 4
            } elseif ($hasType) {
161 2
                $pushUnusedArguments = false;
162
            }
163
        }
164
165 28
        foreach ($arguments as $key => $value) {
166 7
            if (is_int($key)) {
167 7
                if (!is_object($value)) {
168 2
                    throw new InvalidArgumentException((string)$key, $reflection->getName());
169
                }
170 5
                if ($pushUnusedArguments) {
171 2
                    $resolvedArguments[] = $value;
172
                }
173
            }
174
        }
175 26
        return $resolvedArguments;
176
    }
177
}
178