Passed
Pull Request — master (#28)
by Aleksei
12:06
created

Injector::resolveParameter()   C

Complexity

Conditions 14
Paths 20

Size

Total Lines 58
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 14

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 32
c 1
b 0
f 0
nc 20
nop 2
dl 0
loc 58
ccs 28
cts 28
cp 1
crap 14
rs 6.2666

How to fix   Long Method    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 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 49
    public function __construct(ContainerInterface $container)
27
    {
28 49
        $this->container = $container;
29 49
    }
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 36
    public function invoke(callable $callable, array $arguments = [])
60
    {
61 36
        $callable = Closure::fromCallable($callable);
62 36
        $reflection = new ReflectionFunction($callable);
63 36
        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 44
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
125
    {
126 44
        $state = new ResolvingState($reflection, $arguments);
127
128 41
        $isInternalOptional = false;
129 41
        $internalParameter = '';
130 41
        foreach ($reflection->getParameters() as $parameter) {
131 41
            if ($isInternalOptional) {
132 41
                // Check custom parameter definition for an internal function
133 37
                if ($state->hasNamedArgument($parameter->getName())) {
134
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
135 3
                }
136 1
                continue;
137
            }
138 3
            // Resolve parameter
139
            $resolved = $this->resolveParameter($parameter, $state);
140
            if ($resolved === true) {
141 37
                continue;
142 37
            }
143 32
144
            if ($resolved === false) {
145
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
146 8
            }
147 5
            // Internal function. Parameter not resolved
148
            $isInternalOptional = true;
149
            $internalParameter = $parameter->getName();
150 3
        }
151 3
152
        return $state->getResolvedValues();
153
    }
154 34
155 23
    /**
156 34
     * @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
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state): ?bool {
164
        $name = $parameter->getName();
165
        $isVariadic = $parameter->isVariadic();
166
        $hasType = $parameter->hasType();
167
        $state->disableTrailedArguments($isVariadic && $hasType);
168
169 37
        // Try to resolve argument by name
170
        if ($state->resolveParamByName($name, $isVariadic)) {
171
            return true;
172
        }
173
174
        $error = null;
175 37
176 37
        if ($hasType) {
177 37
            $reflectionType = $parameter->getType();
178 37
179
            // $reflectionType may be instance of ReflectionUnionType (php8)
180
            /** @phan-suppress-next-line PhanUndeclaredMethod */
181 37
            $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

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