Passed
Push — master ( 643d29...4f4dd3 )
by Jonathan
08:05 queued 04:07
created

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