Passed
Push — main ( 8d1023...74092d )
by Chema
53s
created

Container::factory()   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
10
use function count;
11
use function get_class;
12
use function is_array;
13
use function is_object;
14
use function is_string;
15
16
class Container implements ContainerInterface
17
{
18
    private AliasRegistry $aliasRegistry;
19
20
    private FactoryManager $factoryManager;
21
22
    private InstanceRegistry $instanceRegistry;
23
24
    private DependencyCacheManager $cacheManager;
25
26
    private BindingResolver $bindingResolver;
27
28
    private DependencyTreeAnalyzer $dependencyTreeAnalyzer;
29
30
    /**
31
     * @param  array<class-string, class-string|callable|object>  $bindings
32
     * @param  array<string, list<Closure>>  $instancesToExtend
33
     */
34 69
    public function __construct(
35
        array $bindings = [],
36
        array $instancesToExtend = [],
37
    ) {
38 69
        $this->aliasRegistry = new AliasRegistry();
39 69
        $this->factoryManager = new FactoryManager($instancesToExtend);
40 69
        $this->instanceRegistry = new InstanceRegistry();
41 69
        $this->bindingResolver = new BindingResolver($bindings);
42 69
        $this->cacheManager = new DependencyCacheManager($bindings);
43 69
        $this->dependencyTreeAnalyzer = new DependencyTreeAnalyzer($this->bindingResolver);
44
    }
45
46
    /**
47
     * @param  class-string  $className
48
     */
49 7
    public static function create(string $className): mixed
50
    {
51 7
        return (new self())->get($className);
52
    }
53
54 54
    public function has(string $id): bool
55
    {
56 54
        $id = $this->aliasRegistry->resolve($id);
57 54
        return $this->instanceRegistry->has($id);
58
    }
59
60 31
    public function set(string $id, mixed $instance): void
61
    {
62 31
        $this->instanceRegistry->set($id, $instance);
63
64 31
        if ($this->factoryManager->isCurrentlyExtending($id)) {
65 3
            return;
66
        }
67
68 31
        $this->extendService($id);
69
    }
70
71
    /**
72
     * @param  class-string|string  $id
73
     */
74 45
    public function get(string $id): mixed
75
    {
76 45
        $id = $this->aliasRegistry->resolve($id);
77
78 45
        if ($this->has($id)) {
79 21
            return $this->instanceRegistry->get($id, $this->factoryManager, $this);
80
        }
81
82 24
        return $this->createInstance($id);
83
    }
84
85 5
    public function resolve(callable $callable): mixed
86
    {
87 5
        $callableKey = $this->callableKey($callable);
88 5
        $closure = Closure::fromCallable($callable);
89
90 5
        $dependencies = $this->cacheManager->resolveCallableDependencies($callableKey, $closure);
91
92
        /** @psalm-suppress MixedMethodCall */
93 5
        return $closure(...$dependencies);
94
    }
95
96 5
    public function factory(Closure $instance): Closure
97
    {
98 5
        $this->factoryManager->markAsFactory($instance);
99
100 5
        return $instance;
101
    }
102
103 3
    public function remove(string $id): void
104
    {
105 3
        $id = $this->aliasRegistry->resolve($id);
106 3
        $this->instanceRegistry->remove($id);
107
    }
108
109 4
    public function alias(string $alias, string $id): void
110
    {
111 4
        $this->aliasRegistry->add($alias, $id);
112
    }
113
114
    /**
115
     * @param class-string $className
116
     *
117
     * @return list<string>
118
     */
119 4
    public function getDependencyTree(string $className): array
120
    {
121 4
        return $this->dependencyTreeAnalyzer->analyze($className);
122
    }
123
124
    /**
125
     * @psalm-suppress MixedAssignment
126
     */
127 12
    public function extend(string $id, Closure $instance): Closure
128
    {
129 12
        $id = $this->aliasRegistry->resolve($id);
130
131 12
        if (!$this->has($id)) {
132 4
            $this->factoryManager->scheduleExtension($id, $instance);
133
134 4
            return $instance;
135
        }
136
137 11
        if ($this->instanceRegistry->isFrozen($id)) {
138 4
            throw ContainerException::frozenInstanceExtend($id);
139
        }
140
141 9
        $factory = $this->instanceRegistry->getRaw($id);
142
143 9
        if ($this->factoryManager->isProtected($factory)) {
144 1
            throw ContainerException::instanceProtected($id);
145
        }
146
147 8
        $extended = $this->factoryManager->generateExtendedInstance($instance, $factory, $this);
148 7
        $this->set($id, $extended);
149
150 7
        $this->factoryManager->transferFactoryStatus($factory, $extended);
151
152 7
        return $extended;
153
    }
154
155 2
    public function protect(Closure $instance): Closure
156
    {
157 2
        $this->factoryManager->markAsProtected($instance);
158
159 2
        return $instance;
160
    }
161
162
    /**
163
     * @return list<string>
164
     */
165 9
    public function getRegisteredServices(): array
166
    {
167 9
        return $this->instanceRegistry->getAll();
168
    }
169
170 6
    public function isFactory(string $id): bool
171
    {
172 6
        $id = $this->aliasRegistry->resolve($id);
173
174 6
        if (!$this->has($id)) {
175 1
            return false;
176
        }
177
178 6
        return $this->factoryManager->isFactory($this->instanceRegistry->getRaw($id));
179
    }
180
181 6
    public function isFrozen(string $id): bool
182
    {
183 6
        $id = $this->aliasRegistry->resolve($id);
184 6
        return $this->instanceRegistry->isFrozen($id);
185
    }
186
187
    /**
188
     * @return array<class-string, class-string|callable|object>
189
     */
190 9
    public function getBindings(): array
191
    {
192 9
        return $this->bindingResolver->getBindings();
193
    }
194
195
    /**
196
     * @param list<class-string> $classNames
197
     */
198 3
    public function warmUp(array $classNames): void
199
    {
200 3
        $this->cacheManager->warmUp($classNames);
201
    }
202
203
    /**
204
     * Get container statistics for debugging and optimization.
205
     *
206
     * @return array{
207
     *     registered_services: int,
208
     *     frozen_services: int,
209
     *     factory_services: int,
210
     *     bindings: int,
211
     *     cached_dependencies: int,
212
     *     memory_usage: string
213
     * }
214
     */
215 8
    public function getStats(): array
216
    {
217 8
        $services = $this->getRegisteredServices();
218 8
        $frozenCount = 0;
219 8
        $factoryCount = 0;
220
221 8
        foreach ($services as $serviceId) {
222 4
            if ($this->isFrozen($serviceId)) {
223 1
                ++$frozenCount;
224
            }
225 4
            if ($this->isFactory($serviceId)) {
226 1
                ++$factoryCount;
227
            }
228
        }
229
230 8
        return [
231 8
            'registered_services' => count($services),
232 8
            'frozen_services' => $frozenCount,
233 8
            'factory_services' => $factoryCount,
234 8
            'bindings' => count($this->getBindings()),
235 8
            'cached_dependencies' => $this->cacheManager->getCacheSize(),
236 8
            'memory_usage' => $this->formatBytes(memory_get_usage(true)),
237 8
        ];
238
    }
239
240 24
    private function createInstance(string $class): ?object
241
    {
242 24
        return $this->bindingResolver->resolve($class, $this->cacheManager);
243
    }
244
245
    /**
246
     * Generates a unique string key for a given callable.
247
     *
248
     * @psalm-suppress MixedReturnTypeCoercion
249
     */
250 5
    private function callableKey(callable $callable): string
251
    {
252 5
        if (is_array($callable)) {
253
            [$classOrObject, $method] = $callable;
254
255
            $className = is_object($classOrObject)
256
                ? get_class($classOrObject) . '#' . spl_object_id($classOrObject)
257
                : $classOrObject;
258
259
            return $className . '::' . $method;
260
        }
261
262 5
        if (is_string($callable)) {
263
            return $callable;
264
        }
265
266 5
        if ($callable instanceof Closure) {
267 5
            return spl_object_hash($callable);
268
        }
269
270
        // Invokable objects
271
        /** @psalm-suppress RedundantCondition */
272
        if (is_object($callable)) {
273
            return get_class($callable) . '#' . spl_object_id($callable);
274
        }
275
276
        // Fallback for edge cases
277
        /** @psalm-suppress MixedArgument */
278
        return 'callable:' . md5(serialize($callable));
279
    }
280
281 31
    private function extendService(string $id): void
282
    {
283 31
        if (!$this->factoryManager->hasPendingExtensions($id)) {
284 28
            return;
285
        }
286
287 3
        $this->factoryManager->setCurrentlyExtending($id);
288
289 3
        foreach ($this->factoryManager->getPendingExtensions($id) as $instance) {
290 3
            $this->extend($id, $instance);
291
        }
292
293 3
        $this->factoryManager->clearPendingExtensions($id);
294 3
        $this->factoryManager->setCurrentlyExtending(null);
295
    }
296
297
    /**
298
     * Format bytes into human-readable format.
299
     */
300 8
    private function formatBytes(int $bytes): string
301
    {
302 8
        $units = ['B', 'KB', 'MB', 'GB'];
303 8
        $bytes = max($bytes, 0);
304 8
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
305 8
        $pow = min($pow, count($units) - 1);
306
307
        /** @var int $powInt */
308 8
        $powInt = (int) $pow;
309 8
        $bytes /= (1 << (10 * $powInt));
310
311 8
        return round($bytes, 2) . ' ' . $units[$powInt];
312
    }
313
}
314