Passed
Pull Request — main (#13)
by Chema
02:25
created

Container   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Test Coverage

Coverage 99.08%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 95
dl 0
loc 258
ccs 108
cts 109
cp 0.9908
rs 8.8798
c 3
b 0
f 0
wmc 44

18 Methods

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