Test Failed
Push — main ( 3d89fd...8d1023 )
by Chema
03:11
created

Container::callableKey()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.027

Importance

Changes 5
Bugs 2 Features 0
Metric Value
eloc 13
c 5
b 2
f 0
dl 0
loc 29
rs 9.2222
ccs 10
cts 11
cp 0.9091
cc 6
nc 6
nop 1
crap 6.027
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use Closure;
8
use Gacela\Container\Exception\ContainerException;
9
10
use function get_class;
11
use function is_array;
12
use function is_object;
13
use function is_string;
14
15
class Container implements ContainerInterface
16
{
17
    private AliasRegistry $aliasRegistry;
18
19
    private FactoryManager $factoryManager;
20
21
    private InstanceRegistry $instanceRegistry;
22
23
    private DependencyCacheManager $cacheManager;
24
25
    private BindingResolver $bindingResolver;
26
27
    private DependencyTreeAnalyzer $dependencyTreeAnalyzer;
28
29
    /**
30
     * @param  array<class-string, class-string|callable|object>  $bindings
31
     * @param  array<string, list<Closure>>  $instancesToExtend
32
     */
33
    public function __construct(
34
        array $bindings = [],
35
        array $instancesToExtend = [],
36
    ) {
37
        $this->aliasRegistry = new AliasRegistry();
38
        $this->factoryManager = new FactoryManager($instancesToExtend);
39
        $this->instanceRegistry = new InstanceRegistry();
40 42
        $this->bindingResolver = new BindingResolver($bindings);
41
        $this->cacheManager = new DependencyCacheManager($bindings);
42
        $this->dependencyTreeAnalyzer = new DependencyTreeAnalyzer($this->bindingResolver);
43
    }
44 42
45 42
    /**
46
     * @param  class-string  $className
47
     */
48
    public static function create(string $className): mixed
49
    {
50
        return (new self())->get($className);
51 5
    }
52
53 5
    public function has(string $id): bool
54
    {
55
        $id = $this->aliasRegistry->resolve($id);
56 37
        return $this->instanceRegistry->has($id);
57
    }
58 37
59
    public function set(string $id, mixed $instance): void
60
    {
61 19
        $this->instanceRegistry->set($id, $instance);
62
63 19
        if ($this->factoryManager->isCurrentlyExtending($id)) {
64 1
            return;
65
        }
66
67 19
        $this->extendService($id);
68
    }
69 19
70 3
    /**
71
     * @param  class-string|string  $id
72
     */
73 19
    public function get(string $id): mixed
74
    {
75
        $id = $this->aliasRegistry->resolve($id);
76
77
        if ($this->has($id)) {
78
            return $this->instanceRegistry->get($id, $this->factoryManager, $this);
79 33
        }
80
81 33
        return $this->createInstance($id);
82 15
    }
83
84
    public function resolve(callable $callable): mixed
85 18
    {
86
        $callableKey = $this->callableKey($callable);
87
        $closure = Closure::fromCallable($callable);
88 5
89
        $dependencies = $this->cacheManager->resolveCallableDependencies($callableKey, $closure);
90 5
91 5
        /** @psalm-suppress MixedMethodCall */
92 5
        return $closure(...$dependencies);
93
    }
94 5
95 5
    public function factory(Closure $instance): Closure
96 5
    {
97 5
        $this->factoryManager->markAsFactory($instance);
98
99
        return $instance;
100
    }
101 5
102
    public function remove(string $id): void
103
    {
104 1
        $id = $this->aliasRegistry->resolve($id);
105
        $this->instanceRegistry->remove($id);
106 1
    }
107
108 1
    public function alias(string $alias, string $id): void
109
    {
110
        $this->aliasRegistry->add($alias, $id);
111 1
    }
112
113 1
    /**
114 1
     * @param class-string $className
115 1
     *
116 1
     * @return list<string>
117
     */
118
    public function getDependencyTree(string $className): array
119
    {
120
        return $this->dependencyTreeAnalyzer->analyze($className);
121
    }
122 11
123
    /**
124 11
     * @psalm-suppress MixedAssignment
125 4
     */
126
    public function extend(string $id, Closure $instance): Closure
127 4
    {
128
        $id = $this->aliasRegistry->resolve($id);
129
130 10
        if (!$this->has($id)) {
131 4
            $this->factoryManager->scheduleExtension($id, $instance);
132
133
            return $instance;
134 8
        }
135 1
136
        if ($this->instanceRegistry->isFrozen($id)) {
137
            throw ContainerException::frozenInstanceExtend($id);
138 7
        }
139 7
140 6
        $factory = $this->instanceRegistry->getRaw($id);
141
142 6
        if ($this->factoryManager->isProtected($factory)) {
143
            throw ContainerException::instanceProtected($id);
144
        }
145 2
146
        $extended = $this->factoryManager->generateExtendedInstance($instance, $factory, $this);
147 2
        $this->set($id, $extended);
148
149 2
        $this->factoryManager->transferFactoryStatus($factory, $extended);
150
151
        return $extended;
152 15
    }
153
154 15
    public function protect(Closure $instance): Closure
155
    {
156 15
        $this->factoryManager->markAsProtected($instance);
157 14
158 15
        return $instance;
159
    }
160 8
161
    /**
162
     * @return list<string>
163 12
     */
164 1
    public function getRegisteredServices(): array
165
    {
166
        return $this->instanceRegistry->getAll();
167 11
    }
168
169
    public function isFactory(string $id): bool
170 11
    {
171
        $id = $this->aliasRegistry->resolve($id);
172 11
173
        if (!$this->has($id)) {
174 11
            return false;
175
        }
176
177 18
        return $this->factoryManager->isFactory($this->instanceRegistry->getRaw($id));
178
    }
179 18
180 4
    public function isFrozen(string $id): bool
181 4
    {
182
        $id = $this->aliasRegistry->resolve($id);
183 2
        return $this->instanceRegistry->isFrozen($id);
184
    }
185 4
186 2
    /**
187
     * @return array<class-string, class-string|callable|object>
188
     */
189
    public function getBindings(): array
190 2
    {
191 2
        return $this->bindingResolver->getBindings();
192
    }
193
194
    /**
195 14
     * @param list<class-string> $classNames
196 10
     */
197
    public function warmUp(array $classNames): void
198
    {
199 4
        $this->cacheManager->warmUp($classNames);
200
    }
201
202
    private function createInstance(string $class): ?object
203
    {
204
        return $this->bindingResolver->resolve($class, $this->cacheManager);
205 12
    }
206
207 12
    /**
208 12
     * Generates a unique string key for a given callable.
209 12
     *
210 12
     * @psalm-suppress MixedReturnTypeCoercion
211 12
     */
212
    private function callableKey(callable $callable): string
213
    {
214
        if (is_array($callable)) {
215 12
            [$classOrObject, $method] = $callable;
216
217
            $className = is_object($classOrObject)
218
                ? get_class($classOrObject) . '#' . spl_object_id($classOrObject)
219
                : $classOrObject;
220
221 4
            return $className . '::' . $method;
222
        }
223 4
224
        if (is_string($callable)) {
225
            return $callable;
226 17
        }
227
228 17
        if ($callable instanceof Closure) {
229 17
            return spl_object_hash($callable);
230 17
        }
231 17
232
        // Invokable objects
233
        /** @psalm-suppress RedundantCondition */
234 17
        if (is_object($callable)) {
235
            return get_class($callable) . '#' . spl_object_id($callable);
236
        }
237
238
        // Fallback for edge cases
239
        /** @psalm-suppress MixedArgument */
240 7
        return 'callable:' . md5(serialize($callable));
241
    }
242 7
243 5
    private function extendService(string $id): void
244 5
    {
245
        if (!$this->factoryManager->hasPendingExtensions($id)) {
246 5
            return;
247 5
        }
248
249
        $this->factoryManager->setCurrentlyExtending($id);
250 4
251 3
        foreach ($this->factoryManager->getPendingExtensions($id) as $instance) {
252
            $this->extend($id, $instance);
253
        }
254 1
255
        $this->factoryManager->clearPendingExtensions($id);
256
        $this->factoryManager->setCurrentlyExtending(null);
257 19
    }
258
}
259