Passed
Push — master ( de3d61...be839c )
by Alec
13:42 queued 13s
created

Container::removeDependencyFromStack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
6
namespace AlecRabbit\Spinner\Container;
7
8
use AlecRabbit\Spinner\Container\Contract\IContainer;
9
use AlecRabbit\Spinner\Container\Contract\IServiceSpawner;
10
use AlecRabbit\Spinner\Container\Exception\CircularDependencyException;
11
use AlecRabbit\Spinner\Container\Exception\ContainerException;
12
use AlecRabbit\Spinner\Container\Exception\NotInContainerException;
13
use ArrayObject;
14
use Closure;
15
use Throwable;
16
use Traversable;
17
18
final class Container implements IContainer
19
{
20
    private IServiceSpawner $serviceSpawner;
21
22
    /** @var ArrayObject<string, callable|object|string> */
23
    private ArrayObject $definitions;
24
25
    /** @var ArrayObject<string, object> */
26
    private ArrayObject $services;
27
28
    private ArrayObject $dependencyStack;
29
30
    /**
31
     * Create a container object with a set of definitions.
32
     */
33
    public function __construct(Closure $spawnerCreatorCb, ?Traversable $definitions = null)
34
    {
35
        $this->serviceSpawner = $spawnerCreatorCb($this);
36
        $this->definitions = new ArrayObject();
37
        $this->services = new ArrayObject();
38
        $this->dependencyStack = new ArrayObject();
39
40
        if ($definitions) {
41
            /** @var callable|object|string $definition */
42
            foreach ($definitions as $id => $definition) {
43
                $this->register($id, $definition);
44
            }
45
        }
46
    }
47
48
    private function register(string $id, mixed $definition): void
49
    {
50
        $this->assertDefinition($definition);
51
52
        $this->assertNotRegistered($id);
53
54
        /** @var callable|object|string $definition */
55
        $this->definitions[$id] = $definition;
56
    }
57
58
    private function assertDefinition(mixed $definition): void
59
    {
60
        if (!is_callable($definition) && !is_object($definition) && !is_string($definition)) {
61
            throw new ContainerException(
62
                sprintf(
63
                    'Definition should be callable, object or string, "%s" given.',
64
                    gettype($definition),
65
                )
66
            );
67
        }
68
    }
69
70
    private function assertNotRegistered(string $id): void
71
    {
72
        if ($this->has($id)) {
73
            throw new ContainerException(
74
                sprintf(
75
                    'Definition with id "%s" already registered in the container.',
76
                    $id,
77
                )
78
            );
79
        }
80
    }
81
82
    public function has(string $id): bool
83
    {
84
        return $this->definitions->offsetExists($id);
85
    }
86
87
    public function replace(string $id, callable|object|string $definition): void
88
    {
89
        $serviceInstantiated = $this->hasService($id);
90
91
        $this->remove($id);
92
        $this->add($id, $definition);
93
        if ($serviceInstantiated) {
94
            $this->get($id); // instantiates service with new definition
95
        }
96
    }
97
98
    private function hasService(string $id): bool
99
    {
100
        return $this->services->offsetExists($id);
101
    }
102
103
    public function remove(string $id): void
104
    {
105
        if (!$this->has($id)) {
106
            throw new NotInContainerException(
107
                sprintf(
108
                    'Definition with id "%s" is not registered in the container.',
109
                    $id,
110
                )
111
            );
112
        }
113
        unset($this->definitions[$id], $this->services[$id]);
114
    }
115
116
    public function add(string $id, callable|object|string $definition): void
117
    {
118
        $this->register($id, $definition);
119
    }
120
121
    public function get(string $id): object
122
    {
123
        if ($this->hasService($id)) {
124
            return $this->services[$id];
125
        }
126
127
        if (!$this->has($id)) {
128
            throw new NotInContainerException(
129
                sprintf(
130
                    'There is no service with id "%s" in the container.',
131
                    $id,
132
                )
133
            );
134
        }
135
136
        $this->addDependencyToStack($id);
137
138
        $definition = $this->definitions[$id];
139
140
        $this->services[$id] = $this->getService($id, $definition);
141
142
        $this->removeDependencyFromStack();
143
144
        return $this->services[$id];
145
    }
146
147
    private function addDependencyToStack(string $id): void
148
    {
149
        $this->assertDependencyIsNotInStack($id);
150
151
        $this->dependencyStack->append($id);
152
    }
153
154
    private function assertDependencyIsNotInStack(string $id): void
155
    {
156
        if (in_array($id, $this->dependencyStack->getArrayCopy(), true)) {
157
            // @codeCoverageIgnoreStart
158
            throw new CircularDependencyException($this->dependencyStack);
159
            // @codeCoverageIgnoreEnd
160
        }
161
    }
162
163
    private function getService(string $id, callable|object|string $definition): object
164
    {
165
        try {
166
            return $this->serviceSpawner->spawn($definition);
167
        } catch (Throwable $e) {
168
            throw new ContainerException(
169
                sprintf(
170
                    'Could not instantiate service with id "%s".%s',
171
                    $id,
172
                    sprintf(
173
                        ' [%s]: "%s".',
174
                        get_debug_type($e),
175
                        $e->getMessage(),
176
                    ),
177
                ),
178
                previous: $e,
179
            );
180
        }
181
    }
182
183
    private function removeDependencyFromStack(): void
184
    {
185
        $this->dependencyStack->offsetUnset($this->dependencyStack->count() - 1);
186
    }
187
}
188