Passed
Pull Request — master (#3)
by Alexander
01:26
created

Injector::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 2
b 0
f 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Injector;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Psr\Container\NotFoundExceptionInterface;
10
use ReflectionException;
11
12
/**
13
 * Injector is able to analyze callable dependencies based on
14
 * type hinting and inject them from any PSR-11 compatible container.
15
 */
16
class Injector
17
{
18
    private ContainerInterface $container;
19
20 30
    public function __construct(ContainerInterface $container)
21
    {
22 30
        $this->container = $container;
23
    }
24
25
    /**
26
     * Invoke a callback with resolving dependencies in parameters.
27
     *
28
     * This methods allows invoking a callback and let type hinted parameter names to be
29
     * resolved as objects of the Container. It additionally allow calling function using named parameters.
30
     *
31
     * For example, the following callback may be invoked using the Container to resolve the formatter dependency:
32
     *
33
     * ```php
34
     * $formatString = function($string, \yii\i18n\Formatter $formatter) {
35
     *    // ...
36
     * }
37
     * $container->invoke($formatString, ['string' => 'Hello World!']);
38
     * ```
39
     *
40
     * This will pass the string `'Hello World!'` as the first param, and a formatter instance created
41
     * by the DI container as the second param to the callable.
42
     *
43
     * @param callable $callback callable to be invoked.
44
     * @param array $params The array of parameters for the function.
45
     * This can be either a list of parameters, or an associative array representing named function parameters.
46
     * @return mixed the callback return value.
47
     * @throws MissingRequiredArgumentException  if required argument is missing.
48
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
49
     * @throws ReflectionException
50
     */
51 30
    public function invoke(callable $callback, array $params = [])
52
    {
53 30
        return \call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
54
    }
55
56
    /**
57
     * Resolve dependencies for a function.
58
     *
59
     * This method can be used to implement similar functionality as provided by [[invoke()]] in other
60
     * components.
61
     *
62
     * @param callable $callback callable to be invoked.
63
     * @param array $parameters The array of parameters for the function, can be either numeric or associative.
64
     * @return array The resolved dependencies.
65
     * @throws MissingRequiredArgumentException if required argument is missing.
66
     * @throws ContainerExceptionInterface if a dependency cannot be resolved or if a dependency cannot be fulfilled.
67
     * @throws ReflectionException
68
     */
69 30
    private function resolveCallableDependencies(callable $callback, array $parameters = []): array
70
    {
71 30
        if (\is_object($callback) && !$callback instanceof \Closure) {
72
            $callback = [$callback, '__invoke'];
73
        }
74
75 30
        if (\is_array($callback)) {
76 2
            $reflection = new \ReflectionMethod($callback[0], $callback[1]);
77
        } else {
78 28
            $reflection = new \ReflectionFunction($callback);
79
        }
80
81 30
        $arguments = [];
82
83 30
        $pushUnusedParams = true;
84 30
        foreach ($reflection->getParameters() as $param) {
85 27
            $name = $param->getName();
86 27
            $class = $param->getClass();
87 27
            $hasType = $param->hasType();
88 27
            $isNullable = $param->allowsNull() && $hasType;
89 27
            $isVariadic = $param->isVariadic();
90 27
            $error = null;
91
92
            // Get argument by name
93 27
            if (array_key_exists($name, $parameters)) {
94 6
                if ($isVariadic && is_array($parameters[$name])) {
95 2
                    $arguments = array_merge($arguments, array_values($parameters[$name]));
96
                } else {
97 4
                    $arguments[] = $parameters[$name];
98
                }
99 6
                unset($parameters[$name]);
100 6
                continue;
101
            }
102
103 24
            if ($class !== null) {
104
                // Unnamed parameters
105 17
                $className = $class->getName();
106 17
                $found = false;
107 17
                foreach ($parameters as $key => $item) {
108 8
                    if (!is_int($key)) {
109 2
                        continue;
110
                    }
111 6
                    if ($item instanceof $className) {
112 5
                        $found = true;
113 5
                        $arguments[] = $item;
114 5
                        unset($parameters[$key]);
115 5
                        if (!$isVariadic) {
116 3
                            break;
117
                        }
118
                    }
119
                }
120 17
                if ($found) {
121 5
                    $pushUnusedParams = false;
122 5
                    continue;
123
                }
124
125
                // If the argument is optional we catch not instantiable exceptions
126
                try {
127 16
                    $arguments[] = $this->container->get($className);
128 14
                    continue;
129 3
                } catch (NotFoundExceptionInterface $e) {
130 3
                    $error = $e;
131
                }
132
            }
133
134 13
            if ($param->isDefaultValueAvailable()) {
135 2
                $arguments[] = $param->getDefaultValue();
136 11
            } elseif (!$param->isOptional()) {
137 8
                if ($isNullable) {
138 2
                    $arguments[] = null;
139
                } else {
140 8
                    throw $error ?? new MissingRequiredArgumentException($name, $reflection->getName());
141
                }
142 3
            } elseif ($hasType) {
143 2
                $pushUnusedParams = false;
144
            }
145
        }
146
147 24
        foreach ($parameters as $key => $value) {
148 5
            if (is_int($key)) {
149 5
                if (!is_object($value)) {
150 2
                    throw new InvalidParameterException((string)$key, $reflection->getName());
151
                }
152 3
                if ($pushUnusedParams) {
153 2
                    $arguments[] = $value;
154
                }
155
            }
156
        }
157 22
        return $arguments;
158
    }
159
}
160