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

Injector::resolveObjectParameter()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

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