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

272
                foreach ($type->/** @scrutinizer ignore-call */ getTypes() as $namedType) {
Loading history...
273
                    $classes[] = $namedType->getName();
274
                }
275
                /** @var array<int, class-string> $classes */
276
                if ($state->resolveParameterByClasses($classes, $variadic)) {
277
                    return true;
278
                }
279
                break;
280
        }
281 13
        return false;
282
    }
283
284
    /**
285
     * @throws ContainerExceptionInterface
286
     * @throws NotFoundExceptionInterface
287
     *
288
     * @return bool True if argument was resolved
289
     */
290 33
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic): bool
291
    {
292 33
        $type = $parameter->getName();
293
        /** @psalm-var class-string|null $class */
294 33
        $class = $parameter->isBuiltin() ? null : $type;
295 33
        $isClass = $class !== null || $type === 'object';
296 33
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic);
297
    }
298
299
    /**
300
     * @psalm-param class-string|null $class
301
     *
302
     * @throws ContainerExceptionInterface
303
     * @throws NotFoundExceptionInterface
304
     *
305
     * @return bool True if argument resolved
306
     */
307 28
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic): bool
308
    {
309 28
        $found = $state->resolveParameterByClass($class, $isVariadic);
310 28
        if ($found || $isVariadic) {
311 13
            return $found;
312
        }
313 21
        if ($class !== null) {
314 20
            $argument = $this->container->get($class);
315 16
            $state->addResolvedValue($argument);
316 16
            return true;
317
        }
318 1
        return false;
319
    }
320
321
    /**
322
     * @psalm-param class-string $class
323
     *
324
     * @throws ReflectionException
325
     */
326 15
    private function getClassReflection(string $class): ReflectionClass
327
    {
328 15
        if ($this->cacheReflections) {
329 1
            return $this->reflectionsCache[$class] ??= new ReflectionClass($class);
330
        }
331
332 14
        return new ReflectionClass($class);
333
    }
334
}
335