Passed
Push — bugfix/support-windows ( 517fb4...0e99e7 )
by Chema
04:47
created

Container::extend()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 4
nop 2
dl 0
loc 21
ccs 12
cts 12
cp 1
crap 5
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>> */
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
36
     */
37 32
    public function __construct(array $servicesToExtend = [])
38
    {
39 32
        $this->servicesToExtend = $servicesToExtend;
40 32
        $this->factoryServices = new SplObjectStorage();
41 32
        $this->protectedServices = new SplObjectStorage();
42
    }
43
44 1
    public function getLocator(): Locator
45
    {
46 1
        return Locator::getInstance();
47
    }
48
49 28
    public function set(string $id, mixed $service): void
50
    {
51 28
        if (isset($this->frozenServices[$id])) {
52 1
            throw ContainerException::serviceFrozen($id);
53
        }
54
55 28
        $this->services[$id] = $service;
56
57 28
        if ($this->currentlyExtending === $id) {
58 4
            return;
59
        }
60
61 28
        $this->extendService($id);
62
    }
63
64 39
    public function has(string $id): bool
65
    {
66 39
        return isset($this->services[$id]);
67
    }
68
69
    /**
70
     * @throws ContainerKeyNotFoundException
71
     */
72 36
    public function get(string $id): mixed
73
    {
74 36
        if (!$this->has($id)) {
75 5
            throw new ContainerKeyNotFoundException($this, $id);
76
        }
77
78 31
        $this->frozenServices[$id] = true;
79
80 31
        if (!is_object($this->services[$id])
81 19
            || isset($this->protectedServices[$this->services[$id]])
82 31
            || !method_exists($this->services[$id], '__invoke')
83
        ) {
84 21
            return $this->services[$id];
85
        }
86
87 16
        if (isset($this->factoryServices[$this->services[$id]])) {
88 1
            return $this->services[$id]($this);
89
        }
90
91 15
        $rawService = $this->services[$id];
92
93
        /** @psalm-suppress InvalidFunctionCall */
94 15
        $this->services[$id] = $rawService($this);
95
96
        /** @var mixed $resolvedService */
97 15
        $resolvedService = $this->services[$id];
98
99 15
        return $resolvedService;
100
    }
101
102 1
    public function factory(Closure $service): Closure
103
    {
104 1
        $this->factoryServices->attach($service);
105
106 1
        return $service;
107
    }
108
109 2
    public function remove(string $id): void
110
    {
111 2
        unset(
112 2
            $this->services[$id],
113 2
            $this->frozenServices[$id]
114 2
        );
115
    }
116
117
    /**
118
     * @psalm-suppress MixedAssignment
119
     */
120 12
    public function extend(string $id, Closure $service): Closure
121
    {
122 12
        if (!$this->has($id)) {
123 3
            $this->extendLater($id, $service);
124
125 3
            return $service;
126
        }
127
128 11
        if (isset($this->frozenServices[$id])) {
129 4
            throw ContainerException::serviceFrozen($id);
130
        }
131
132 9
        if (is_object($this->services[$id]) && isset($this->protectedServices[$this->services[$id]])) {
133 1
            throw ContainerException::serviceProtected($id);
134
        }
135
136 8
        $factory = $this->services[$id];
137 8
        $extended = $this->generateExtendedService($service, $factory);
138 7
        $this->set($id, $extended);
139
140 7
        return $extended;
141
    }
142
143 2
    public function protect(Closure $service): Closure
144
    {
145 2
        $this->protectedServices->attach($service);
146
147 2
        return $service;
148
    }
149
150 3
    private function extendLater(string $id, Closure $service): void
151
    {
152 3
        if (!isset($this->servicesToExtend[$id])) {
153 3
            $this->servicesToExtend[$id] = [];
154
        }
155
156 3
        $this->servicesToExtend[$id][] = $service;
157
    }
158
159
    /**
160
     * @psalm-suppress MissingClosureReturnType,MixedAssignment
161
     */
162 8
    private function generateExtendedService(Closure $service, mixed $factory): Closure
163
    {
164 8
        if (is_callable($factory)) {
165 6
            return static function (self $container) use ($service, $factory) {
166 5
                $r1 = $factory($container);
167 5
                $r2 = $service($r1, $container);
168
169 5
                return $r2 ?? $r1;
170 6
            };
171
        }
172
173 6
        if (is_object($factory) || is_array($factory)) {
174 5
            return static function (self $container) use ($service, $factory) {
175 4
                $r = $service($factory, $container);
176
177 4
                return $r ?? $factory;
178 5
            };
179
        }
180
181 1
        throw ContainerException::serviceNotExtendable();
182
    }
183
184 28
    private function extendService(string $id): void
185
    {
186 28
        if (!isset($this->servicesToExtend[$id]) || count($this->servicesToExtend[$id]) === 0) {
187 28
            return;
188
        }
189 4
        $this->currentlyExtending = $id;
190
191 4
        foreach ($this->servicesToExtend[$id] as $service) {
192 4
            $extended = $this->extend($id, $service);
193
        }
194
195 4
        unset($this->servicesToExtend[$id]);
196 4
        $this->currentlyExtending = null;
197
198 4
        $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