Passed
Pull Request — master (#78)
by Sergei
02:43
created

Injector::resolveParameter()   B

Complexity

Conditions 11
Paths 24

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11.0055

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 28
nc 24
nop 2
dl 0
loc 49
ccs 27
cts 28
cp 0.9643
crap 11.0055
rs 7.3166
c 2
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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