Passed
Push — feature/container-extend ( b81097...93c762 )
by Chema
03:26
created

Container::extendLater()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
ccs 4
cts 4
cp 1
crap 2
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_callable;
14
use function is_object;
15
16
final class Container implements ContainerInterface
17
{
18
    /** @var array<string,mixed> */
19
    private array $raw = [];
20
21
    /** @var array<string,mixed> */
22
    private array $services = [];
23
24
    private SplObjectStorage $factoryServices;
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
    private ?string $currentlyExtending = null;
30
31
    /**
32
     * @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...
33
     */
34 23
    public function __construct(array $servicesToExtend = [])
35
    {
36 23
        $this->servicesToExtend = $servicesToExtend;
37 23
        $this->factoryServices = new SplObjectStorage();
38
    }
39
40 1
    public function getLocator(): Locator
41
    {
42 1
        return Locator::getInstance();
43
    }
44
45 18
    public function set(string $id, $service): void
46
    {
47 18
        $this->services[$id] = $service;
48
49 18
        if ($this->currentlyExtending === $id) {
50 1
            return;
51
        }
52
53 18
        $this->extendService($id);
54
    }
55
56 31
    public function has(string $id): bool
57
    {
58 31
        return isset($this->services[$id]);
59
    }
60
61
    /**
62
     * @throws ContainerKeyNotFoundException
63
     *
64
     * @return mixed
65
     */
66 29
    public function get(string $id)
67
    {
68 29
        if (!$this->has($id)) {
69 5
            throw new ContainerKeyNotFoundException($this, $id);
70
        }
71
72
        if (
73 24
            isset($this->raw[$id])
74 15
            || !is_object($this->services[$id])
75 24
            || !method_exists($this->services[$id], '__invoke')
76
        ) {
77 19
            return $this->services[$id];
78
        }
79
80 10
        if (isset($this->factoryServices[$this->services[$id]])) {
81 1
            return $this->services[$id]($this);
82
        }
83
84 9
        $rawService = $this->services[$id];
85
86
        /** @psalm-suppress InvalidFunctionCall */
87 9
        $this->services[$id] = $rawService($this);
88
89
        /** @var mixed $resolvedService */
90 9
        $resolvedService = $this->services[$id];
91 9
        $this->raw[$id] = $rawService;
92
93 9
        return $resolvedService;
94
    }
95
96 2
    public function factory(object $service): object
97
    {
98 2
        if (!method_exists($service, '__invoke')) {
99 1
            throw ContainerException::serviceNotInvokable();
100
        }
101
102 1
        $this->factoryServices->attach($service);
103
104 1
        return $service;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $service returns the type object which is incompatible with the type-hinted return object.
Loading history...
105
    }
106
107 2
    public function remove(string $id): void
108
    {
109
        unset(
110 2
            $this->raw[$id],
111 2
            $this->services[$id]
112
        );
113
    }
114
115
    /**
116
     * @psalm-suppress MixedAssignment
117
     */
118 5
    public function extend(string $id, Closure $service): Closure
119
    {
120 5
        if (!$this->has($id)) {
121 1
            $this->extendLater($id, $service);
122
123 1
            return $service;
124
        }
125
126 4
        $factory = $this->services[$id];
127 4
        $extended = $this->generateExtendedService($service, $factory);
128 3
        $this->set($id, $extended);
129
130 3
        return $extended;
131
    }
132
133 1
    private function extendLater(string $id, Closure $service): void
134
    {
135 1
        if (!isset($this->servicesToExtend[$id])) {
136 1
            $this->servicesToExtend[$id] = [];
137
        }
138
139 1
        $this->servicesToExtend[$id][] = $service;
140
    }
141
142
    /**
143
     * @psalm-suppress MissingClosureReturnType,MixedAssignment
144
     *
145
     * @param mixed $factory
146
     */
147 4
    private function generateExtendedService(Closure $service, $factory): Closure
148
    {
149 4
        if (is_callable($factory)) {
150 4
            return static function (self $container) use ($service, $factory) {
151 3
                $r1 = $factory($container);
152 3
                $r2 = $service($r1, $container);
153
154 3
                return $r2 ?? $r1;
155
            };
156
        }
157
158 3
        if (is_object($factory)) {
159 2
            return static function (self $container) use ($service, $factory) {
160 2
                $r = $service($factory, $container);
161
162 2
                return $r ?? $factory;
163
            };
164
        }
165
166 1
        throw ContainerException::serviceNotExtendable();
167
    }
168
169 18
    private function extendService(string $id): void
170
    {
171 18
        if (!isset($this->servicesToExtend[$id]) || count($this->servicesToExtend[$id]) === 0) {
172 18
            return;
173
        }
174 1
        $this->currentlyExtending = $id;
175
176 1
        foreach ($this->servicesToExtend[$id] as $service) {
177 1
            $extended = $this->extend($id, $service);
178
        }
179
180 1
        unset($this->servicesToExtend[$id]);
181 1
        $this->currentlyExtending = null;
182
183 1
        $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 176. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
184
    }
185
}
186