Passed
Push — master ( bbbf0c...d384f9 )
by Jonathan
03:29
created

Container::resolve()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 15
rs 9.6111
ccs 8
cts 8
cp 1
cc 5
nc 4
nop 2
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gravatalonga\Container;
6
7
use ArrayAccess;
8
use Closure;
9
use Psr\Container\ContainerInterface;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionFunction;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
use Reflector;
16
17
use function array_key_exists;
18
19
/**
20
 * Class Container.
21
 */
22
class Container implements ArrayAccess, ContainerInterface
23
{
24
    /**
25
     * @var ContainerInterface
26
     */
27
    protected static $instance;
28
29
    /**
30
     * @var array<string, mixed>
31
     */
32
    private $bindings;
33
34
    /**
35
     * @var array<string, mixed>
36
     */
37
    private $resolved = [];
38
39
    /**
40
     * @var array<string, mixed>
41
     */
42
    private $share;
43
44
    /**
45
     * Container constructor.
46
     *
47
     * @param array<string, mixed> $config
48
     */
49 96
    public function __construct(array $config = [])
50
    {
51 96
        $this->bindings = $config;
52 96
        $this->share = [];
53 96
    }
54
55
    /**
56
     * Factory binding.
57
     *
58
     * @param string $id
59
     * @param Closure $factory
60
     *
61
     * @return void
62
     */
63 36
    public function factory($id, Closure $factory)
64
    {
65 36
        $this->bindings[$id] = $factory;
66 36
    }
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @throws ReflectionException
72
     */
73 72
    public function get($id)
74
    {
75 72
        return $this->resolve($id, []);
76
    }
77
78
    /**
79
     * @return ContainerInterface
80
     */
81 3
    public static function getInstance()
82
    {
83 3
        return self::$instance;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 87
    public function has($id)
90
    {
91 87
        return array_key_exists($id, $this->bindings) || array_key_exists($id, $this->share);
92
    }
93
94
    /**
95
     * @param string $id
96
     * @param array<string, mixed> $arguments
97
     *
98
     * @throws NotFoundContainerException
99
     * @throws ContainerException|ReflectionException
100
     *
101
     * @return mixed|object
102
     */
103 15
    public function make($id, array $arguments = [])
104
    {
105 15
        if (array_key_exists($id, $this->share)) {
106 3
            throw ContainerException::shareOnMake($id);
107
        }
108
109 12
        return $this->resolve($id, $arguments);
110
    }
111
112
    /**
113
     * @param string $offset
114
     *
115
     * @return bool
116
     */
117 3
    public function offsetExists($offset)
118
    {
119 3
        return $this->has($offset);
120
    }
121
122
    /**
123
     * @param string $offset
124
     *
125
     * @throws ReflectionException
126
     *
127
     * @return mixed
128
     */
129 6
    public function offsetGet($offset)
130
    {
131 6
        return $this->get($offset);
132
    }
133
134
    /**
135
     * @param string $offset
136
     * @param mixed $value
137
     */
138 9
    public function offsetSet($offset, $value): void
139
    {
140 9
        $this->factory($offset, $value);
141 9
    }
142
143
    /**
144
     * @param string $offset
145
     *
146
     * @return void
147
     */
148 3
    public function offsetUnset($offset)
149
    {
150 3
        unset($this->bindings[$offset], $this->share[$offset], $this->resolved[$offset]);
151 3
    }
152
153
    /**
154
     * Alias for Factory method.
155
     *
156
     * @param string $id
157
     * @param mixed $factory
158
     *
159
     * @return void
160
     */
161 27
    public function set($id, $factory)
162
    {
163 27
        if ($factory instanceof Closure) {
164 9
            $this->factory($id, $factory);
165
166 9
            return;
167
        }
168 21
        $this->bindings[$id] = $factory;
169 21
    }
170
171
    /**
172
     * @param ContainerInterface $container
173
     */
174 3
    public static function setInstance(ContainerInterface $container): void
175
    {
176 3
        self::$instance = $container;
177 3
    }
178
179
    /**
180
     * Share rather resolve as factory.
181
     *
182
     * @param string $id
183
     * @param Closure $factory
184
     *
185
     * @return void
186
     */
187 15
    public function share($id, Closure $factory)
188
    {
189 15
        $this->share[$id] = $factory;
190 15
    }
191
192
    /**
193
     * @param ReflectionParameter[] $params
194
     * @param array<string, mixed> $arguments
195
     *
196
     * @return array<int, string>
197
     */
198 72
    private function buildDependencies(array $params, array $arguments = [])
199
    {
200 72
        return array_map(
201
            function (ReflectionParameter $param) use ($arguments) {
202 45
                if (true === array_key_exists($param->getName(), $arguments)) {
203 3
                    return $arguments[$param->getName()];
204
                }
205
206
                /** @var ReflectionNamedType|null $type */
207 42
                $type = $param->getType();
208
209
                // in case we can't find type hint, we guess by variable name.
210
                // e.g.: $cache it will attempt resolve 'cache' from container.
211 42
                if (null === $type) {
212 9
                    if ($this->has($param->getName())) {
213 6
                        return $this->get($param->getName());
214
                    }
215
216 3
                    throw ContainerException::findType(null);
217
                }
218
219 36
                if (true === $type->isBuiltin()) {
220 15
                    if ($this->has($param->getName())) {
221 9
                        return $this->get($param->getName());
222
                    }
223
224 6
                    if ($type->allowsNull()) {
225 3
                        return null;
226
                    }
227
228 3
                    throw ContainerException::findType($type);
229
                }
230
231 21
                if (ContainerInterface::class === $type->getName()) {
232 18
                    return $this;
233
                }
234
235 3
                return $this->get($type->getName());
236 72
            },
237 48
            $params
238
        );
239
    }
240
241
    /**
242
     * @param string $id
243
     * @param array<string, mixed> $arguments
244
     *
245
     * @throws NotFoundContainerException
246
     * @throws ReflectionException
247
     *
248
     * @return mixed|object
249
     */
250 81
    private function resolve(string $id, array $arguments = [])
251
    {
252 81
        if (isset($this->resolved[$id])) {
253 3
            return $this->resolved[$id];
254
        }
255
256 81
        if (!$this->has($id) && class_exists($id)) {
257 33
            return $this->resolveClass($id, $arguments);
258
        }
259
260 63
        if ($this->has($id)) {
261 60
            return $this->resolveEntry($id, $arguments);
262
        }
263
264 3
        throw NotFoundContainerException::entryNotFound($id);
265
    }
266
267
    /**
268
     * @param Reflector $reflection
269
     * @param array<string, mixed> $arguments
270
     *
271
     * @return array<int, mixed>
272
     */
273 72
    private function resolveArguments(Reflector $reflection, array $arguments = [])
274
    {
275 72
        $params = [];
276
277 72
        if ($reflection instanceof ReflectionClass) {
278 33
            if (!$constructor = $reflection->getConstructor()) {
279 3
                $params = [];
280
            } else {
281 33
                $params = $constructor->getParameters();
282
            }
283 42
        } elseif ($reflection instanceof ReflectionFunction) {
284 42
            if (!$params = $reflection->getParameters()) {
285 27
                $params = [];
286
            }
287
        }
288
289 72
        return $this->buildDependencies($params, $arguments);
290
    }
291
292
    /**
293
     * @param class-string|object $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|object.
Loading history...
294
     * @param array<string, mixed> $arguments
295
     *
296
     * @throws ReflectionException
297
     *
298
     * @return object
299
     */
300 33
    private function resolveClass($id, array $arguments = [])
301
    {
302 33
        $reflection = new ReflectionClass($id);
303
304 33
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
305
    }
306
307
    /**
308
     * @param string $id
309
     * @param array<string, mixed> $arguments
310
     *
311
     * @throws ReflectionException
312
     *
313
     * @return mixed
314
     */
315 60
    private function resolveEntry(string $id, array $arguments = [])
316
    {
317 60
        $get = $this->bindings[$id] ?? $this->share[$id];
318
319 60
        if ($get instanceof Closure) {
320 42
            $reflection = new ReflectionFunction($get);
321 42
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
322
323 42
            if (isset($this->share[$id])) {
324 9
                $this->resolved[$id] = $value;
325
            }
326
327 42
            return $value;
328
        }
329
330 21
        return $get;
331
    }
332
}
333