Container::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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