Passed
Pull Request — master (#31)
by
unknown
13:42
created

Injector::resolveObjectParameter()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 14
c 2
b 0
f 0
nc 5
nop 4
dl 0
loc 20
ccs 0
cts 0
cp 0
crap 72
rs 8.4444
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
    private bool $contextual;
26 50
27
    public function __construct(ContainerInterface $container, bool $contextual = true)
28 50
    {
29 50
        $this->container = $container;
30
        $this->contextual = $contextual;
31
    }
32
33
    /**
34
     * Invoke a callback with resolving dependencies based on parameter types.
35
     *
36
     * This methods allows invoking a callback and let type hinted parameter names to be
37
     * resolved as objects of the Container. It additionally allow calling function passing named arguments.
38
     *
39
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
40
     *
41
     * ```php
42
     * $formatString = function($string, \Yiisoft\I18n\MessageFormatterInterface $formatter) {
43
     *    // ...
44
     * }
45
     *
46
     * $injector = new Yiisoft\Injector\Injector($container);
47
     * $injector->invoke($formatString, ['string' => 'Hello World!']);
48
     * ```
49
     *
50
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
51
     * by the DI container as the second argument.
52
     *
53
     * @param callable $callable callable to be invoked.
54
     * @param array $arguments The array of the function arguments.
55
     * This can be either a list of arguments, or an associative array where keys are argument names.
56
     * @return mixed the callable return value.
57
     * @throws MissingRequiredArgumentException if required argument is missing.
58
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
59 37
     * @throws ReflectionException
60
     */
61 37
    public function invoke(callable $callable, array $arguments = [])
62 37
    {
63 37
        $callable = Closure::fromCallable($callable);
64
        $reflection = new ReflectionFunction($callable);
65
        return $reflection->invokeArgs($this->resolveDependencies($reflection, $arguments));
66
    }
67
68
    /**
69
     * Creates an object of a given class with resolving constructor dependencies based on parameter types.
70
     *
71
     * This methods allows invoking a constructor and let type hinted parameter names to be
72
     * resolved as objects of the Container. It additionally allow calling constructor passing named arguments.
73
     *
74
     * For example, the following constructor may be invoked using the Container to resolve the formatter dependency:
75
     *
76
     * ```php
77
     * class StringFormatter
78
     * {
79
     *     public function __construct($string, \Yiisoft\I18n\MessageFormatterInterface $formatter)
80
     *     {
81
     *         // ...
82
     *     }
83
     * }
84
     *
85
     * $injector = new Yiisoft\Injector\Injector($container);
86
     * $stringFormatter = $injector->make(StringFormatter::class, ['string' => 'Hello World!']);
87
     * ```
88
     *
89
     * This will pass the string `'Hello World!'` as the first argument, and a formatter instance created
90
     * by the DI container as the second argument.
91
     *
92
     * @param string $class name of the class to be created.
93
     * @param array $arguments The array of the function arguments.
94
     * This can be either a list of arguments, or an associative array where keys are argument names.
95
     * @return mixed object of the given class.
96
     * @throws ContainerExceptionInterface
97
     * @throws MissingRequiredArgumentException|InvalidArgumentException
98 13
     * @throws ReflectionException
99
     */
100 13
    public function make(string $class, array $arguments = [])
101 12
    {
102 3
        $classReflection = new ReflectionClass($class);
103
        if (!$classReflection->isInstantiable()) {
104 9
            throw new \InvalidArgumentException("Class $class is not instantiable.");
105 9
        }
106
        $reflection = $classReflection->getConstructor();
107 1
        if ($reflection === null) {
108
            // Method __construct() does not exist
109
            return new $class();
110 8
        }
111
112
        return new $class(...$this->resolveDependencies($reflection, $arguments));
113
    }
114
115
    public function withContextual(bool $contextual): self
116
    {
117
        if ($contextual === $this->contextual) {
118
            return $this;
119
        }
120
121
        $new = clone $this;
122
        $new->contextual = $contextual;
123
        return $this;
124 45
    }
125
126 45
    /**
127
     * Resolve dependencies for the given function reflection object and a list of concrete arguments
128 42
     * and return array of arguments to call the function with.
129 42
     *
130 42
     * @param ReflectionFunctionAbstract $reflection function reflection.
131 38
     * @param array $arguments concrete arguments.
132
     * @return array resolved arguments.
133 3
     * @throws ContainerExceptionInterface
134 1
     * @throws MissingRequiredArgumentException|InvalidArgumentException
135
     * @throws ReflectionException
136 3
     */
137
    private function resolveDependencies(ReflectionFunctionAbstract $reflection, array $arguments = []): array
138
    {
139 38
        $state = new ResolvingState($reflection, $arguments);
140 38
141 33
        $isInternalOptional = false;
142
        $internalParameter = '';
143
        foreach ($reflection->getParameters() as $parameter) {
144 8
            if ($isInternalOptional) {
145 5
                // Check custom parameter definition for an internal function
146
                if ($state->hasNamedArgument($parameter->getName())) {
147
                    throw new MissingInternalArgumentException($reflection, $internalParameter);
148 3
                }
149 3
                continue;
150
            }
151
            // Resolve parameter
152 35
            $resolved = $this->resolveParameter($parameter, $state);
153
            if ($resolved === true) {
154
                continue;
155
            }
156
157
            if ($resolved === false) {
158
                throw new MissingRequiredArgumentException($reflection, $parameter->getName());
159
            }
160
            // Internal function. Parameter not resolved
161
            $isInternalOptional = true;
162
            $internalParameter = $parameter->getName();
163 38
        }
164
165 38
        return $state->getResolvedValues();
166 38
    }
167 38
168 38
    /**
169
     * @param ReflectionParameter $parameter
170
     * @param ResolvingState $state
171 38
     * @return null|bool True if argument resolved; False if not resolved; Null if parameter is optional but without
172 12
     * default value in a Reflection object. This is possible for internal functions.
173
     * @throws NotFoundExceptionInterface
174
     * @throws ReflectionException
175 32
     */
176
    private function resolveParameter(ReflectionParameter $parameter, ResolvingState $state): ?bool
177 32
    {
178 28
        $name = $parameter->getName();
179
        $isVariadic = $parameter->isVariadic();
180
        $hasType = $parameter->hasType();
181
        $state->disablePushTrailingArguments($isVariadic && $hasType);
182 28
183 28
        // Try to resolve parameter by name
184
        if ($state->resolveParameterByName($name, $isVariadic)) {
185 28
            return true;
186 27
        }
187
188 4
        $error = null;
189 4
190
        if ($hasType) {
191
            $reflectionType = $parameter->getType();
192
193
            // $reflectionType may be instance of ReflectionUnionType (php8)
194 16
            /** @phan-suppress-next-line PhanUndeclaredMethod */
195 2
            $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

195
            $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...
196 2
            foreach ($types as $namedType) {
197 2
                try {
198
                    if ($this->resolveNamedType($state, $namedType, $isVariadic, $name)) {
199
                        return true;
200 14
                    }
201 10
                } catch (NotFoundExceptionInterface $e) {
202 4
                    $error = $e;
203 4
                }
204 4
            }
205
        }
206
207 6
        if ($parameter->isDefaultValueAvailable()) {
208 5
            $argument = $parameter->getDefaultValue();
209
            $state->addResolvedValue($argument);
210
            return true;
211
        }
212 1
213
        if (!$parameter->isOptional()) {
214
            if ($hasType && $parameter->allowsNull()) {
215 4
                $argument = null;
216 1
                $state->addResolvedValue($argument);
217
                return true;
218 3
            }
219
220
            if ($error === null) {
221
                return false;
222
            }
223
224
            // Throw container exception
225
            throw $error;
226
        }
227 28
228
        if ($isVariadic) {
229 28
            return true;
230 28
        }
231 28
        return null;
232 28
    }
233
234
    /**
235
     * @param ResolvingState $state
236
     * @param ReflectionNamedType $parameter
237
     * @param bool $isVariadic
238
     * @param string $name
239
     * @return bool True if argument was resolved
240
     */
241
    private function resolveNamedType(ResolvingState $state, ReflectionNamedType $parameter, bool $isVariadic, string $name): bool
242 24
    {
243
        $type = $parameter->getName();
244 24
        $class = $parameter->isBuiltin() ? null : $type;
245 24
        $isClass = $class !== null || $type === 'object';
246 13
        return $isClass && $this->resolveObjectParameter($state, $class, $isVariadic, $name);
247
    }
248 17
249 16
    /**
250 14
     * @param ResolvingState $state
251 14
     * @param null|string $class
252
     * @param bool $isVariadic
253 1
     * @param string $name
254
     * @return bool True if argument resolved
255
     * @throws ContainerExceptionInterface
256
     */
257
    private function resolveObjectParameter(ResolvingState $state, ?string $class, bool $isVariadic, string $name): bool
258
    {
259
        $found = $state->resolveParameterByClass($class, $isVariadic);
260
        if ($found || $isVariadic) {
261
            return $found;
262
        }
263
        if ($class !== null) {
264
            if ($this->contextual && $class !== ContainerInterface::class && $class !== Injector::class) {
265
                try {
266
                    $argument = $this->container->get("$class\$$name");
267
                } catch (NotFoundExceptionInterface $e) {
268
                    $argument = $this->container->get($class);
269
                }
270
            } else {
271
                $argument = $this->container->get($class);
272
            }
273
            $state->addResolvedValue($argument);
274
            return true;
275
        }
276
        return false;
277
    }
278
}
279