Passed
Pull Request — master (#63)
by Aleksei
02:16
created

Injector   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Test Coverage

Coverage 92.11%

Importance

Changes 13
Bugs 0 Features 1
Metric Value
eloc 72
dl 0
loc 251
ccs 70
cts 76
cp 0.9211
rs 9.76
c 13
b 0
f 1
wmc 33

7 Methods

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