Passed
Push — master ( 7655a2...08803f )
by Alexander
07:55 queued 06:29
created

Injector   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 10
Bugs 0 Features 1
Metric Value
eloc 72
c 10
b 0
f 1
dl 0
loc 232
ccs 77
cts 77
cp 1
rs 9.76
wmc 33

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A invoke() 0 5 1
A make() 0 13 3
A resolveObjectParameter() 0 12 4
C resolveParameter() 0 56 14
A resolveDependencies() 0 29 6
A resolveNamedType() 0 6 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Injector;
6
7
use Closure;
8
use Psr\Container\ContainerExceptionInterface;
9
use Psr\Container\ContainerInterface;
10
use Psr\Container\NotFoundExceptionInterface;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionFunction;
14
use ReflectionFunctionAbstract;
15
use ReflectionNamedType;
16
use ReflectionParameter;
17
18
/**
19
 * Injector is able to analyze callable dependencies based on type hinting and
20
 * inject them from any PSR-11 compatible container.
21
 */
22
final class Injector
23
{
24
    private ContainerInterface $container;
25
26 50
    public function __construct(ContainerInterface $container)
27
    {
28 50
        $this->container = $container;
29 50
    }
30
31
    /**
32
     * Invoke a callback with resolving dependencies based on parameter types.
33
     *
34
     * This methods allows invoking a callback and let type hinted parameter names to be
35
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
36
     *
37
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
38
     *
39
     * ```php
40
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
41
     *    // ...
42
     * }
43
     *
44
     * $injector = new Yiisoft\Injector\Injector($container);
45
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
46
     * ```
47
     *
48
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
49
     * by the DI container as the second argument.
50
     *
51
     * @param callable $callable callable to be invoked.
52
     * @param array $arguments The array of the function arguments.
53
     * This can be either a list of arguments, or an associative array where keys are argument names.
54
     * @return mixed the callable return value.
55
     * @throws MissingRequiredArgumentException if required argument is missing.
56
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
57
     * @throws ReflectionException
58
     */
59 37
    public function invoke(callable $callable, array $arguments = [])
60
    {
61 37
        $callable = Closure::fromCallable($callable);
62 37
        $reflection = new ReflectionFunction($callable);
63 37
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
64
    }
65
66
    /**
67
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
68
     *
69
     * This methods allows invoking a constructor and let type hinted parameter names to be
70
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
71
     *
72
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
73
     *
74
     * ```php
75
     * class StringFormatter
76
     * {
77
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
78
     *     {
79
     *         // ...
80
     *     }
81
     * }
82
     *
83
     * $injector = new Yiisoft\Injector\Injector($container);
84
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
85
     * ```
86
     *
87
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
88
     * by the DI container as the second argument.
89
     *
90
     * @param string $class name of the class to be created.
91
     * @param array $arguments The array of the function arguments.
92
     * This can be either a list of arguments, or an associative array where keys are argument names.
93
     * @return mixed object of the given class.
94
     * @throws ContainerExceptionInterface
95
     * @throws MissingRequiredArgumentException|InvalidArgumentException
96
     * @throws ReflectionException
97
     */
98 13
    public function make(string $class, array $arguments = [])
99
    {
100 13
        $classReflection = new ReflectionClass($class);
101 12
        if (!$classReflection->isInstantiable()) {
102 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
103
        }
104 9
        $reflection = $classReflection->getConstructor();
105 9
        if ($reflection === null) {
106
            // Method __construct() does not exist
107 1
            return new $class();
108
        }
109
110 8
        return new $class(...$this->resolveDependencies($reflection, $arguments));
111
    }
112
113
    /**
114
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
115
     * and return array of arguments to call the function with.
116
     *
117
     * @param ReflectionFunctionAbstract $reflection function reflection.
118
     * @param array $arguments concrete arguments.
119
     * @return array resolved arguments.
120
     * @throws ContainerExceptionInterface
121
     * @throws MissingRequiredArgumentException|InvalidArgumentException
122
     * @throws ReflectionException
123
     */
124 45
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
125
    {
126 45
        $state = new ResolvingState($reflection, $arguments);
127
128 42
        $isInternalOptional = false;
129 42
        $internalParameter = '';
130 42
        foreach ($reflection->getParameters() as $parameter) {
131 38
            if ($isInternalOptional) {
132
                // Check custom parameter definition for an internal function
133 3
                if ($state->hasNamedArgument($parameter->getName())) {
134 1
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
135
                }
136 3
                continue;
137
            }
138
            // Resolve parameter
139 38
            $resolved = $this->resolveParameter($parameter, $state);
140 38
            if ($resolved === true) {
141 33
                continue;
142
            }
143
144 8
            if ($resolved === false) {
145 5
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
146
            }
147
            // Internal function. Parameter not resolved
148 3
            $isInternalOptional = true;
149 3
            $internalParameter = $parameter->getName();
150
        }
151
152 35
        return $state->getResolvedValues();
153
    }
154
155
    /**
156
     * @param ReflectionParameter $parameter
157
     * @param ResolvingState $state
158
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
159
     * default value in a Reflection object. This is possible for internal functions.
160
     * @throws NotFoundExceptionInterface
161
     * @throws ReflectionException
162
     */
163 38
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state): ?bool
164
    {
165 38
        $name = $parameter->getName();
166 38
        $isVariadic = $parameter->isVariadic();
167 38
        $hasType = $parameter->hasType();
168 38
        $state->disablePushTrailingArguments($isVariadic && $hasType);
169
170
        // Try to resolve parameter by name
171 38
        if ($state->resolveParameterByName($name, $isVariadic)) {
172 12
            return true;
173
        }
174
175 32
        $error = null;
176
177 32
        if ($hasType) {
178 28
            $reflectionType = $parameter->getType();
179
180
            // $reflectionType may be instance of ReflectionUnionType (php8)
181
            /** @phan-suppress-next-line PhanUndeclaredMethod */
182 28
            $types = $reflectionType instanceof ReflectionNamedType ? [$reflectionType] : $reflectionType->getTypes();
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
            $types = $reflectionType instanceof ReflectionNamedType ? [$reflectionType] : $reflectionType->/** @scrutinizer ignore-call */ getTypes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
183 28
            foreach ($types as $namedType) {
184
                try {
185 28
                    if ($this->resolveNamedType($state, $namedType, $isVariadic)) {
186 27
                        return true;
187
                    }
188 4
                } catch (NotFoundExceptionInterface $e) {
189 4
                    $error = $e;
190
                }
191
            }
192
        }
193
194 16
        if ($parameter->isDefaultValueAvailable()) {
195 2
            $argument = $parameter->getDefaultValue();
196 2
            $state->addResolvedValue($argument);
197 2
            return true;
198
        }
199
200 14
        if (!$parameter->isOptional()) {
201 10
            if ($hasType && $parameter->allowsNull()) {
202 4
                $argument = null;
203 4
                $state->addResolvedValue($argument);
204 4
                return true;
205
            }
206
207 6
            if ($error === null) {
208 5
                return false;
209
            }
210
211
            // Throw container exception
212 1
            throw $error;
213
        }
214
215 4
        if ($isVariadic) {
216 1
            return true;
217
        }
218 3
        return null;
219
    }
220
221
    /**
222
     * @param ResolvingState $state
223
     * @param ReflectionNamedType $parameter
224
     * @param bool $isVariadic
225
     * @return bool True if argument was resolved
226
     */
227 28
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic): bool
228
    {
229 28
        $type = $parameter->getName();
230 28
        $class = $parameter->isBuiltin() ? null : $type;
231 28
        $isClass = $class !== null || $type === 'object';
232 28
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic);
233
    }
234
235
    /**
236
     * @param ResolvingState $state
237
     * @param null|string $class
238
     * @param bool $isVariadic
239
     * @return bool True if argument resolved
240
     * @throws ContainerExceptionInterface
241
     */
242 24
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic): bool
243
    {
244 24
        $found = $state->resolveParameterByClass($class, $isVariadic);
245 24
        if ($found || $isVariadic) {
246 13
            return $found;
247
        }
248 17
        if ($class !== null) {
249 16
            $argument = $this->container->get($class);
250 14
            $state->addResolvedValue($argument);
251 14
            return true;
252
        }
253 1
        return false;
254
    }
255
}
256