Passed
Push — main ( edbce0...46740f )
by Chema
01:03 queued 14s
created

Container::protect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
rs 10
ccs 3
cts 3
cp 1
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use Closure;
8
use Gacela\Container\Exception\ContainerException;
9
use SplObjectStorage;
10
11
use function count;
12
use function is_array;
13
use function is_callable;
14
use function is_object;
15
16
final class Container implements ContainerInterface
17
{
18
    private ?DependencyResolver $dependencyResolver = null;
19
20
    /** @var array<class-string,list<mixed>> */
21
    private array $cachedDependencies = [];
22
23
    /** @var array<string,mixed> */
24
    private array $instances = [];
25
26
    private SplObjectStorage $factoryInstances;
27
28
    private SplObjectStorage $protectedInstances;
29
30
    /** @var array<string,bool> */
31
    private array $frozenInstances = [];
32
33
    private ?string $currentlyExtending = null;
34
35
    /**
36
     * @param array<class-string, class-string|callable|object> $bindings
1 ignored issue
show
Documentation Bug introduced by
The doc comment array<class-string, class-string|callable|object> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, class-string|callable|object>.
Loading history...
37
     * @param array<string, list<Closure>> $instancesToExtend
38
     */
39 33
    public function __construct(
40
        private array $bindings = [],
41
        private array $instancesToExtend = [],
42
    ) {
43 33
        $this->factoryInstances = new SplObjectStorage();
44 33
        $this->protectedInstances = new SplObjectStorage();
45
    }
46
47
    /**
48
     * @param class-string $className
49
     */
50 2
    public static function create(string $className): mixed
51
    {
52 2
        return (new self())->get($className);
53
    }
54
55 33
    public function has(string $id): bool
56
    {
57 33
        return isset($this->instances[$id]);
58
    }
59
60 18
    public function set(string $id, mixed $instance): void
61
    {
62 18
        if (isset($this->frozenInstances[$id])) {
63 1
            throw ContainerException::instanceFrozen($id);
64
        }
65
66 18
        $this->instances[$id] = $instance;
67
68 18
        if ($this->currentlyExtending === $id) {
69 2
            return;
70
        }
71
72 18
        $this->extendService($id);
73
    }
74
75
    /**
76
     * @param class-string|string $id
77
     */
78 29
    public function get(string $id): mixed
79
    {
80 29
        if ($this->has($id)) {
81 14
            return $this->getInstance($id);
82
        }
83
84 15
        return $this->createInstance($id);
85
    }
86
87 1
    public function factory(Closure $instance): Closure
88
    {
89 1
        $this->factoryInstances->attach($instance);
90
91 1
        return $instance;
92
    }
93
94 1
    public function remove(string $id): void
95
    {
96 1
        unset(
97 1
            $this->instances[$id],
98 1
            $this->frozenInstances[$id]
99 1
        );
100
    }
101
102
    /**
103
     * @psalm-suppress MixedAssignment
104
     */
105 10
    public function extend(string $id, Closure $instance): Closure
106
    {
107 10
        if (!$this->has($id)) {
108 3
            $this->extendLater($id, $instance);
109
110 3
            return $instance;
111
        }
112
113 9
        if (isset($this->frozenInstances[$id])) {
114 4
            throw ContainerException::instanceFrozen($id);
115
        }
116
117 7
        if (is_object($this->instances[$id]) && isset($this->protectedInstances[$this->instances[$id]])) {
118 1
            throw ContainerException::instanceProtected($id);
119
        }
120
121 6
        $factory = $this->instances[$id];
122 6
        $extended = $this->generateExtendedInstance($instance, $factory);
123 5
        $this->set($id, $extended);
124
125 5
        return $extended;
126
    }
127
128 2
    public function protect(Closure $instance): Closure
129
    {
130 2
        $this->protectedInstances->attach($instance);
131
132 2
        return $instance;
133
    }
134
135 14
    private function getInstance(string $id): mixed
136
    {
137 14
        $this->frozenInstances[$id] = true;
138
139 14
        if (!is_object($this->instances[$id])
140 13
            || isset($this->protectedInstances[$this->instances[$id]])
141 14
            || !method_exists($this->instances[$id], '__invoke')
142
        ) {
143 8
            return $this->instances[$id];
144
        }
145
146 11
        if (isset($this->factoryInstances[$this->instances[$id]])) {
147 1
            return $this->instances[$id]($this);
148
        }
149
150 10
        $rawService = $this->instances[$id];
151
152
        /** @psalm-suppress InvalidFunctionCall */
153 10
        $this->instances[$id] = $rawService($this);
154
155
        /** @var mixed $resolvedService */
156 10
        $resolvedService = $this->instances[$id];
157
158 10
        return $resolvedService;
159
    }
160
161 15
    private function createInstance(string $class): ?object
162
    {
163 15
        if (isset($this->bindings[$class])) {
164 4
            $binding = $this->bindings[$class];
165 4
            if (is_callable($binding)) {
166
                /** @var mixed $binding */
167 2
                $binding = $binding();
168
            }
169 4
            if (is_object($binding)) {
170 2
                return $binding;
171
            }
172
173
            /** @var class-string $binding */
174 2
            if (class_exists($binding)) {
175 2
                return $this->instantiateClass($binding);
176
            }
177
        }
178
179 11
        if (class_exists($class)) {
180 7
            return $this->instantiateClass($class);
181
        }
182
183 4
        return null;
184
    }
185
186
    /**
187
     * @param class-string $class
188
     */
189 9
    private function instantiateClass(string $class): ?object
190
    {
191 9
        if (class_exists($class)) {
192 9
            if (!isset($this->cachedDependencies[$class])) {
193 9
                $this->cachedDependencies[$class] = $this
194 9
                    ->getDependencyResolver()
195 9
                    ->resolveDependencies($class);
196
            }
197
198
            /** @psalm-suppress MixedMethodCall */
199 9
            return new $class(...$this->cachedDependencies[$class]);
200
        }
201
202
        return null;
203
    }
204
205 3
    private function extendLater(string $id, Closure $instance): void
206
    {
207 3
        if (!isset($this->instancesToExtend[$id])) {
208 3
            $this->instancesToExtend[$id] = [];
209
        }
210
211 3
        $this->instancesToExtend[$id][] = $instance;
212
    }
213
214 9
    private function getDependencyResolver(): DependencyResolver
215
    {
216 9
        if ($this->dependencyResolver === null) {
217 9
            $this->dependencyResolver = new DependencyResolver(
218 9
                $this->bindings,
219 9
            );
220
        }
221
222 9
        return $this->dependencyResolver;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->dependencyResolver could return the type null which is incompatible with the type-hinted return Gacela\Container\DependencyResolver. Consider adding an additional type-check to rule them out.
Loading history...
223
    }
224
225
    /**
226
     * @psalm-suppress MissingClosureReturnType,MixedAssignment
227
     */
228 6
    private function generateExtendedInstance(Closure $instance, mixed $factory): Closure
229
    {
230 6
        if (is_callable($factory)) {
231 4
            return static function (self $container) use ($instance, $factory) {
232 4
                $r1 = $factory($container);
233 4
                $r2 = $instance($r1, $container);
234
235 4
                return $r2 ?? $r1;
236 4
            };
237
        }
238
239 4
        if (is_object($factory) || is_array($factory)) {
240 3
            return static function (self $container) use ($instance, $factory) {
241 3
                $r = $instance($factory, $container);
242
243 3
                return $r ?? $factory;
244 3
            };
245
        }
246
247 1
        throw ContainerException::instanceNotExtendable();
248
    }
249
250 18
    private function extendService(string $id): void
251
    {
252 18
        if (!isset($this->instancesToExtend[$id]) || count($this->instancesToExtend[$id]) === 0) {
253 18
            return;
254
        }
255 2
        $this->currentlyExtending = $id;
256
257 2
        foreach ($this->instancesToExtend[$id] as $instance) {
258 2
            $extended = $this->extend($id, $instance);
259
        }
260
261 2
        unset($this->instancesToExtend[$id]);
262 2
        $this->currentlyExtending = null;
263
264 2
        $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 257. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
265
    }
266
}
267