Passed
Push — master ( e05046...fea23e )
by Jonathan
03:48
created

Container::alias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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