Passed
Pull Request — master (#52)
by Dmitriy
02:19
created

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

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