Passed
Pull Request — master (#64)
by Aleksei
07:27 queued 05:18
created

Injector::invoke()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
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 ReflectionIntersectionType;
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use ReflectionNamedType;
17
use ReflectionParameter;
18
use ReflectionType;
19
use ReflectionUnionType;
20
21
/**
22
 * Injector is able to analyze callable dependencies based on type hinting and
23
 * inject them from any PSR-11 compatible container.
24
 */
25
final class Injector
26
{
27
    private ContainerInterface $container;
28
29 55
    public function __construct(ContainerInterface $container)
30
    {
31 55
        $this->container = $container;
32
    }
33
34
    /**
35
     * Invoke a callback with resolving dependencies based on parameter types.
36
     *
37
     * This methods allows invoking a callback and let type hinted parameter names to be
38
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
39
     *
40
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
41
     *
42
     * ```php
43
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
44
     *    // ...
45
     * }
46
     *
47
     * $injector = new Yiisoft\Injector\Injector($container);
48
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
49
     * ```
50
     *
51
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
52
     * by the DI container as the second argument.
53
     *
54
     * @param callable $callable callable to be invoked.
55
     * @param array $arguments The array of the function arguments.
56
     * This can be either a list of arguments, or an associative array where keys are argument names.
57
     *
58
     * @throws MissingRequiredArgumentException if required argument is missing.
59
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
60
     * @throws ReflectionException
61
     *
62
     * @return mixed the callable return value.
63
     */
64 41
    public function invoke(callable $callable, array $arguments = [])
65
    {
66 41
        $callable = Closure::fromCallable($callable);
67 41
        $reflection = new ReflectionFunction($callable);
68 41
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
69
    }
70
71
    /**
72
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
73
     *
74
     * This methods allows invoking a constructor and let type hinted parameter names to be
75
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
76
     *
77
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
78
     *
79
     * ```php
80
     * class StringFormatter
81
     * {
82
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
83
     *     {
84
     *         // ...
85
     *     }
86
     * }
87
     *
88
     * $injector = new Yiisoft\Injector\Injector($container);
89
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
90
     * ```
91
     *
92
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
93
     * by the DI container as the second argument.
94
     *
95
     * @param string $class name of the class to be created.
96
     * @param array $arguments The array of the function arguments.
97
     * This can be either a list of arguments, or an associative array where keys are argument names.
98
     *
99
     * @throws ContainerExceptionInterface
100
     * @throws InvalidArgumentException|MissingRequiredArgumentException
101
     * @throws ReflectionException
102
     *
103
     * @return object The object of the given class.
104
     *
105
     * @psalm-suppress MixedMethodCall
106
     *
107
     * @psalm-template T
108
     * @psalm-param class-string<T> $class
109
     * @psalm-return T
110
     */
111 14
    public function make(string $class, array $arguments = []): object
112
    {
113 14
        $classReflection = new ReflectionClass($class);
114 13
        if (!$classReflection->isInstantiable()) {
115 3
            throw new \InvalidArgumentException("Class $class is not instantiable.");
116
        }
117 10
        $reflection = $classReflection->getConstructor();
118 10
        if ($reflection === null) {
119
            // Method __construct() does not exist
120 1
            return new $class();
121
        }
122
123 9
        return new $class(...$this->resolveDependencies($reflection, $arguments));
124
    }
125
126
    /**
127
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
128
     * and return array of arguments to call the function with.
129
     *
130
     * @param ReflectionFunctionAbstract $reflection function reflection.
131
     * @param array $arguments concrete arguments.
132
     *
133
     * @throws ContainerExceptionInterface
134
     * @throws InvalidArgumentException|MissingRequiredArgumentException
135
     * @throws ReflectionException
136
     *
137
     * @return array resolved arguments.
138
     */
139 50
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
140
    {
141 50
        $state = new ResolvingState($reflection, $arguments);
142
143 47
        $isInternalOptional = false;
144 47
        $internalParameter = '';
145 47
        foreach ($reflection->getParameters() as $parameter) {
146 39
            if ($isInternalOptional) {
147
                // Check custom parameter definition for an internal function
148
                if ($state->hasNamedArgument($parameter->getName())) {
149
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
150
                }
151
                continue;
152
            }
153
            // Resolve parameter
154 39
            $resolved = $this->resolveParameter($parameter, $state);
155 39
            if ($resolved === true) {
156 36
                continue;
157
            }
158
159 5
            if ($resolved === false) {
160 5
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
161
            }
162
            // Internal function. Parameter not resolved
163
            $isInternalOptional = true;
164
            $internalParameter = $parameter->getName();
165
        }
166
167 41
        return $state->getResolvedValues();
168
    }
169
170
    /**
171
     * @throws NotFoundExceptionInterface
172
     * @throws ReflectionException
173
     *
174
     * @return bool|null {@see true} if argument resolved; False if not resolved; Null if parameter is optional but
175
     * without default value in a Reflection object. This is possible for internal functions.
176
     */
177 39
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state): ?bool
178
    {
179 39
        $name = $parameter->getName();
180 39
        $isVariadic = $parameter->isVariadic();
181 39
        $hasType = $parameter->hasType();
182 39
        $state->disablePushTrailingArguments($isVariadic && $hasType);
183
184
        // Try to resolve parameter by name
185 39
        if ($state->resolveParameterByName($name, $isVariadic)) {
186 12
            return true;
187
        }
188
189 33
        $error = null;
190
191 33
        if ($hasType) {
192
            /** @var ReflectionType $reflectionType */
193 32
            $reflectionType = $parameter->getType();
194
195 32
            if ($this->resolveParameterType($state, $reflectionType, $isVariadic, $error)) {
196 23
                return true;
197
            }
198
        }
199
200 16
        if ($parameter->isDefaultValueAvailable()) {
201
            /** @var mixed $argument */
202 5
            $argument = $parameter->getDefaultValue();
203 5
            $state->addResolvedValue($argument);
204 5
            return true;
205
        }
206
207 11
        if (!$parameter->isOptional()) {
208 10
            if ($hasType && $parameter->allowsNull()) {
209 4
                $argument = null;
210 4
                $state->addResolvedValue($argument);
211 4
                return true;
212
            }
213
214 6
            if ($error === null) {
215 5
                return false;
216
            }
217
218
            // Throw NotFoundExceptionInterface
219 1
            throw $error;
220
        }
221
222 1
        if ($isVariadic) {
223 1
            return true;
224
        }
225
        return null;
226
    }
227
228
    /**
229
     * Resolve parameter using its type.
230
     *
231
     * @param NotFoundExceptionInterface|null $error Last caught {@see NotFoundExceptionInterface} exception.
232
     *
233
     * @throws ContainerExceptionInterface
234
     *
235
     * @return bool {@see true} if argument was resolved
236
     *
237
     * @psalm-suppress MixedAssignment
238
     * @psalm-suppress PossiblyUndefinedMethod
239
     */
240 32
    private function resolveParameterType(
241
        ResolvingState $state,
242
        ReflectionType $type,
243
        bool $variadic,
244
        ?NotFoundExceptionInterface &$error
245
    ): bool {
246
        switch (true) {
247 32
            case $type instanceof ReflectionNamedType:
248 31
                $types = [$type];
249
                // no break
250 1
            case $type instanceof ReflectionUnionType:
251 32
                $types ??= $type->getTypes();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $types does not seem to be defined for all execution paths leading up to this point.
Loading history...
252
                /** @var array<int, ReflectionNamedType> $types */
253 32
                foreach ($types as $namedType) {
254
                    try {
255 32
                        if ($this->resolveNamedType($state, $namedType, $variadic)) {
256 31
                            return true;
257
                        }
258 6
                    } catch (NotFoundExceptionInterface $e) {
259 6
                        $error = $e;
260
                    }
261
                }
262 13
                break;
263
            case $type instanceof ReflectionIntersectionType:
264
                $classes = [];
265
                /** @var ReflectionNamedType $namedType */
266
                foreach ($type->getTypes() as $namedType) {
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

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

266
                foreach ($type->/** @scrutinizer ignore-call */ getTypes() as $namedType) {
Loading history...
267
                    $classes[] = $namedType->getName();
268
                }
269
                /** @var array<int, class-string> $classes */
270
                if ($state->resolveParameterByClasses($classes, $variadic)) {
271
                    return true;
272
                }
273
                break;
274
        }
275 13
        return false;
276
    }
277
278
    /**
279
     * @throws ContainerExceptionInterface
280
     * @throws NotFoundExceptionInterface
281
     *
282
     * @return bool {@see true} if argument was resolved
283
     */
284 32
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic): bool
285
    {
286 32
        $type = $parameter->getName();
287
        /** @psalm-var class-string|null $class */
288 32
        $class = $parameter->isBuiltin() ? null : $type;
289 32
        $isClass = $class !== null || $type === 'object';
290 32
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic);
291
    }
292
293
    /**
294
     * @psalm-param class-string|null $class
295
     *
296
     * @throws ContainerExceptionInterface
297
     * @throws NotFoundExceptionInterface
298
     *
299
     * @return bool True if argument resolved
300
     */
301 27
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic): bool
302
    {
303 27
        $found = $state->resolveParameterByClass($class, $isVariadic);
304 27
        if ($found || $isVariadic) {
305 13
            return $found;
306
        }
307 20
        if ($class !== null) {
308
            /** @var mixed $argument */
309 19
            $argument = $this->container->get($class);
310 15
            $state->addResolvedValue($argument);
311 15
            return true;
312
        }
313 1
        return false;
314
    }
315
}
316