Passed
Pull Request — master (#32)
by Aleksei
14:34
created

Injector::resolveObjectParameter()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

204
            $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...
205
            foreach ($types as $namedType) {
206
                try {
207 6
                    if ($this->resolveNamedType($state, $parameter, $namedType)) {
208 5
                        return true;
209
                    }
210
                } catch (NotFoundExceptionInterface $e) {
211
                    $error = $e;
212 1
                }
213
            }
214
        }
215 4
216 1
        if ($parameter->isDefaultValueAvailable()) {
217
            $argument = $parameter->getDefaultValue();
218 3
            $state->addResolvedValue($argument);
219
            return true;
220
        }
221
222
        if (!$parameter->isOptional()) {
223
            if ($hasType && $parameter->allowsNull()) {
224
                $argument = null;
225
                $state->addResolvedValue($argument);
226
                return true;
227 28
            }
228
229 28
            if ($error === null) {
230 28
                return false;
231 28
            }
232 28
233
            // Throw container exception
234
            throw $error;
235
        }
236
237
        if ($isVariadic) {
238
            return true;
239
        }
240
        return null;
241
    }
242 24
243
    /**
244 24
     * @param ResolvingState $state
245 24
     * @param ReflectionParameter $parameter
246 13
     * @param ReflectionNamedType $namedType
247
     * @param bool $isVariadic
248 17
     * @return bool True if argument was resolved
249 16
     */
250 14
    private function resolveNamedType(
251 14
        ResolvingState $state,
252
        ReflectionParameter $parameter,
253 1
        ReflectionNamedType $namedType
254
    ): bool {
255
        $type = $namedType->getName();
256
        $class = $namedType->isBuiltin() ? null : $type;
257
        $isClass = $class !== null || $type === 'object';
258
        return $isClass && $this->resolveObjectParameter($state, $parameter, $class);
259
    }
260
261
    /**
262
     * @param ResolvingState $state
263
     * @param ReflectionParameter $parameter
264
     * @param null|string $class
265
     * @return bool True if argument resolved
266
     */
267
    private function resolveObjectParameter(ResolvingState $state, ReflectionParameter $parameter, ?string $class): bool
268
    {
269
        $isVariadic = $parameter->isVariadic();
270
        $found = $state->resolveParameterByClass($class, $isVariadic);
271
        if ($found || $isVariadic) {
272
            return $found;
273
        }
274
        if ($class !== null) {
275
            $toReplace = $state->getDataToTemplate() + [
276
                self::TEMPLATE_PARAM_CLASS => $class,
277
                self::TEMPLATE_PARAM_NAME => $parameter->getName(),
278
                self::TEMPLATE_PARAM_POS => (string) $parameter->getPosition(),
279
            ];
280
            $left = count($this->idTemplates);
281
            foreach ($this->idTemplates as $template) {
282
                try {
283
                    --$left;
284
                    $argument = $this->container->get(strtr($template, $toReplace));
285
                    break;
286
                } catch (NotFoundExceptionInterface $e) {
287
                    if ($left === 0) {
288
                        throw $e;
289
                    }
290
                }
291
            }
292
            $state->addResolvedValue($argument);
293
            return true;
294
        }
295
        return false;
296
    }
297
}
298