Passed
Push — main ( 08d8b4...a03e47 )
by Chema
57s queued 14s
created

Container   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 253
Duplicated Lines 0 %

Test Coverage

Coverage 99.07%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 94
c 3
b 0
f 0
dl 0
loc 253
rs 8.96
wmc 43
ccs 106
cts 107
cp 0.9907

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A create() 0 3 1
A protect() 0 5 1
A remove() 0 5 1
A has() 0 3 1
A resolve() 0 14 2
A extendService() 0 13 4
A createInstance() 0 23 6
A set() 0 13 3
A getDependencyResolver() 0 9 2
A getInstance() 0 23 5
A extend() 0 21 5
A get() 0 7 2
A generateExtendedInstance() 0 15 4
A extendLater() 0 3 1
A factory() 0 5 1
A instantiateClass() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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