Passed
Branch main (e7910b)
by Chema
04:11
created

Container::extend()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 4
nop 2
dl 0
loc 21
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Framework\Container;
6
7
use Closure;
8
use Gacela\Framework\Container\Exception\ContainerException;
9
use Gacela\Framework\Container\Exception\ContainerKeyNotFoundException;
10
use SplObjectStorage;
11
12
use function count;
13
use function is_array;
14
use function is_callable;
15
use function is_object;
16
17
final class Container implements ContainerInterface
18
{
19
    /** @var array<string,mixed> */
20
    private array $services = [];
21
22
    private SplObjectStorage $factoryServices;
23
24
    private SplObjectStorage $protectedServices;
25
26
    /** @var array<string,list<Closure>> */
1 ignored issue
show
Documentation Bug introduced by
The doc comment array<string,list<Closure>> at position 4 could not be parsed: Expected '>' at position 4, but found 'list'.
Loading history...
27
    private array $servicesToExtend;
28
29
    /** @var array<string,bool> */
30
    private array $frozenServices = [];
31
32
    private ?string $currentlyExtending = null;
33
34
    /**
35
     * @param array<string,list<Closure>> $servicesToExtend
1 ignored issue
show
Documentation Bug introduced by
The doc comment array<string,list<Closure>> at position 4 could not be parsed: Expected '>' at position 4, but found 'list'.
Loading history...
36
     */
37
    public function __construct(array $servicesToExtend = [])
38
    {
39
        $this->servicesToExtend = $servicesToExtend;
40
        $this->factoryServices = new SplObjectStorage();
41
        $this->protectedServices = new SplObjectStorage();
42
    }
43
44
    public function getLocator(): Locator
45
    {
46
        return Locator::getInstance();
47
    }
48
49
    public function set(string $id, mixed $service): void
50
    {
51
        if (isset($this->frozenServices[$id])) {
52
            throw ContainerException::serviceFrozen($id);
53
        }
54
55
        $this->services[$id] = $service;
56
57
        if ($this->currentlyExtending === $id) {
58
            return;
59
        }
60
61
        $this->extendService($id);
62
    }
63
64
    public function has(string $id): bool
65
    {
66
        return isset($this->services[$id]);
67
    }
68
69
    /**
70
     * @throws ContainerKeyNotFoundException
71
     */
72
    public function get(string $id): mixed
73
    {
74
        if (!$this->has($id)) {
75
            throw new ContainerKeyNotFoundException($this, $id);
76
        }
77
78
        $this->frozenServices[$id] = true;
79
80
        if (!is_object($this->services[$id])
81
            || isset($this->protectedServices[$this->services[$id]])
82
            || !method_exists($this->services[$id], '__invoke')
83
        ) {
84
            return $this->services[$id];
85
        }
86
87
        if (isset($this->factoryServices[$this->services[$id]])) {
88
            return $this->services[$id]($this);
89
        }
90
91
        $rawService = $this->services[$id];
92
93
        /** @psalm-suppress InvalidFunctionCall */
94
        $this->services[$id] = $rawService($this);
95
96
        /** @var mixed $resolvedService */
97
        $resolvedService = $this->services[$id];
98
99
        return $resolvedService;
100
    }
101
102
    public function factory(Closure $service): Closure
103
    {
104
        $this->factoryServices->attach($service);
105
106
        return $service;
107
    }
108
109
    public function remove(string $id): void
110
    {
111
        unset(
112
            $this->services[$id],
113
            $this->frozenServices[$id]
114
        );
115
    }
116
117
    /**
118
     * @psalm-suppress MixedAssignment
119
     */
120
    public function extend(string $id, Closure $service): Closure
121
    {
122
        if (!$this->has($id)) {
123
            $this->extendLater($id, $service);
124
125
            return $service;
126
        }
127
128
        if (isset($this->frozenServices[$id])) {
129
            throw ContainerException::serviceFrozen($id);
130
        }
131
132
        if (is_object($this->services[$id]) && isset($this->protectedServices[$this->services[$id]])) {
133
            throw ContainerException::serviceProtected($id);
134
        }
135
136
        $factory = $this->services[$id];
137
        $extended = $this->generateExtendedService($service, $factory);
138
        $this->set($id, $extended);
139
140
        return $extended;
141
    }
142
143
    public function protect(Closure $service): Closure
144
    {
145
        $this->protectedServices->attach($service);
146
147
        return $service;
148
    }
149
150
    private function extendLater(string $id, Closure $service): void
151
    {
152
        if (!isset($this->servicesToExtend[$id])) {
153
            $this->servicesToExtend[$id] = [];
154
        }
155
156
        $this->servicesToExtend[$id][] = $service;
157
    }
158
159
    /**
160
     * @psalm-suppress MissingClosureReturnType,MixedAssignment
161
     */
162
    private function generateExtendedService(Closure $service, mixed $factory): Closure
163
    {
164
        if (is_callable($factory)) {
165
            return static function (self $container) use ($service, $factory) {
166
                $r1 = $factory($container);
167
                $r2 = $service($r1, $container);
168
169
                return $r2 ?? $r1;
170
            };
171
        }
172
173
        if (is_object($factory) || is_array($factory)) {
174
            return static function (self $container) use ($service, $factory) {
175
                $r = $service($factory, $container);
176
177
                return $r ?? $factory;
178
            };
179
        }
180
181
        throw ContainerException::serviceNotExtendable();
182
    }
183
184
    private function extendService(string $id): void
185
    {
186
        if (!isset($this->servicesToExtend[$id]) || count($this->servicesToExtend[$id]) === 0) {
187
            return;
188
        }
189
        $this->currentlyExtending = $id;
190
191
        foreach ($this->servicesToExtend[$id] as $service) {
192
            $extended = $this->extend($id, $service);
193
        }
194
195
        unset($this->servicesToExtend[$id]);
196
        $this->currentlyExtending = null;
197
198
        $this->set($id, $extended);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $extended seems to be defined by a foreach iteration on line 191. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
199
    }
200
}
201