Passed
Branch master (2d2b40)
by Divine Niiquaye
02:25
created

AutowireTrait::autowire()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\Traits;
19
20
use Nette\Utils\Callback;
21
use Nette\Utils\Reflection;
22
use Rade\DI\Exceptions\ContainerResolutionException;
23
use Rade\DI\Resolvers\AutowireValueResolver;
24
25
/**
26
 * The autowiring service used in Container class.
27
 *
28
 * @author Divine Niiquaye Ibok <[email protected]>
29
 */
30
trait AutowireTrait
31
{
32
    /** @var array<string,mixed> service name => instance */
33
    private array $values = [];
34
35
    /** @var array<string,bool> service name => bool */
36
    private array $loading = [];
37
38
    /** @var array<string,bool> service name => bool */
39
    private array $frozen = [];
40
41
    /** @var array<string,bool> service name => bool */
42
    private array $keys = [];
43
44
    /** @var string[] alias => service name */
45
    protected array $aliases = [];
46
47
    /** @var array[] tag name => service name => tag value */
48
    protected array $tags = [];
49
50
    /** @var array<string,mixed> service name => instance */
51
    private array $factories = [];
52
53
    /** @var array<string,mixed> service name => instance */
54
    private array $raw = [];
55
56
    private AutowireValueResolver $resolver;
57
58
    /**
59
     * Creates new instance from class string or callable using autowiring.
60
     *
61
     * @param string|callable|object  $callback
62
     * @param array<int|string,mixed> $args
63
     *
64
     * @throws ContainerResolutionException
65
     *
66
     * @return mixed
67
     */
68 44
    public function call($callback, array $args = [])
69
    {
70
        try {
71
            /** @var callable $callback */
72 44
            $callable = Callback::toReflection($callback);
73 17
        } catch (\ReflectionException $e) {
74 17
            if (\is_string($callback)) {
75 16
                return $this->autowireClass($callback, $args);
76
            }
77
78 1
            throw new ContainerResolutionException($e->getMessage());
79
        }
80
81 43
        return $callback(...$this->autowireArguments($callable, $args));
82
    }
83
84
    /**
85
     * Add classes/interfaces for autowiring a service.
86
     *
87
     * @param string   $id The registered service id
88
     * @param string[] $types
89
     *
90
     * @return static
91
     */
92 1
    public function autowire(string $id, array $types)
93
    {
94 1
        if (!$this->offsetExists($id)) {
0 ignored issues
show
Bug introduced by
It seems like offsetExists() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

94
        if (!$this->/** @scrutinizer ignore-call */ offsetExists($id)) {
Loading history...
95 1
            throw new ContainerResolutionException("Service id '{$id}' is not found in container");
96
        }
97
98 1
        $this->resolver->autowire($id, $types);
99
100 1
        return $this;
101
    }
102
103
    /**
104
     * Add a class or interface that should be excluded from autowiring.
105
     *
106
     * @param string ...$types
107
     */
108 2
    public function exclude(string ...$types): void
109
    {
110 2
        foreach ($types as $type) {
111 2
            $this->resolver->exclude($type);
112
        }
113 2
    }
114
115
    /**
116
     * @param mixed $definition
117
     *
118
     * @return mixed
119
     */
120 44
    private function autowireService(string $id, $definition)
121
    {
122
        try {
123 44
            $types = Reflection::getReturnTypes(Callback::toReflection($definition));
124 19
        } catch (\ReflectionException $e) {
125 19
            $types = [\get_class($definition)];
126
        }
127
128
        // Resolving wiring so we could call the service parent classes and interfaces.
129 44
        $this->resolver->autowire($id, $types);
130
131 44
        return $definition;
132
    }
133
134
    /**
135
     * Resolves arguments for callable
136
     *
137
     * @param \ReflectionFunctionAbstract $function
138
     * @param array<int|string,mixed>     $args
139
     *
140
     * @return array<int,mixed>
141
     */
142 47
    private function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array
143
    {
144 47
        $resolvedParameters   = [];
145 47
        $reflectionParameters = $function->getParameters();
146
147 47
        foreach ($reflectionParameters as $parameter) {
148 33
            $position = $parameter->getPosition();
149 33
            $resolved = $this->resolver->resolve($parameter, $args);
150
151 27
            if ($parameter->isVariadic() && (\is_array($resolved) && \count($resolved) > 1)) {
152 2
                foreach (\array_chunk($resolved, 1) as $index => [$value]) {
153 2
                    $resolvedParameters[$index + 1] = $value;
154
                }
155
156 2
                continue;
157
            }
158
159 27
            $resolvedParameters[$position] = $resolved;
160
161 27
            if (empty(\array_diff_key($reflectionParameters, $resolvedParameters))) {
162
                // Stop traversing: all parameters are resolved
163 26
                return $resolvedParameters;
164
            }
165
        }
166
167 18
        return $resolvedParameters;
168
    }
169
170
    /**
171
     * @param string                  $class
172
     * @param array<int|string,mixed> $args
173
     *
174
     * @return object
175
     */
176 22
    private function autowireClass(string $class, array $args)
177
    {
178
        /** @var class-string $class */
179 22
        $reflection = new \ReflectionClass($class);
180
181 22
        if (!$reflection->isInstantiable()) {
182 2
            throw new ContainerResolutionException("Class $class is not instantiable.");
183
        }
184
185 22
        if (null !== $constructor = $reflection->getConstructor()) {
186 19
            return $reflection->newInstanceArgs($this->autowireArguments($constructor, $args));
187
        }
188
189 9
        if (!empty($args)) {
190 1
            throw new ContainerResolutionException("Unable to pass arguments, class $class has no constructor.", \strlen($class));
191
        }
192
193 8
        return new $class();
194
    }
195
}
196