Passed
Push — main ( 74092d...02c73f )
by Chema
15:22 queued 14:38
created

DependencyCacheManager   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Test Coverage

Coverage 85%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 52
c 1
b 0
f 0
dl 0
loc 178
ccs 51
cts 60
cp 0.85
rs 10
wmc 22

10 Methods

Rating   Name   Duplication   Size   Complexity  
A createInstance() 0 10 2
A resolveDependencies() 0 9 2
A getCachedClasses() 0 3 1
A getDependencyResolver() 0 10 2
A instantiate() 0 30 5
A warmUp() 0 12 4
A resolveCallableDependencies() 0 9 2
A getCacheSize() 0 3 1
A __construct() 0 4 1
A hasAttribute() 0 10 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gacela\Container;
6
7
use Closure;
8
use Gacela\Container\Attribute\Factory;
9
use Gacela\Container\Attribute\Singleton;
10
use ReflectionClass;
11
12
use function class_exists;
13
use function count;
14
15
/**
16
 * Manages dependency resolution caching for performance optimization.
17
 */
18
final class DependencyCacheManager
19
{
20
    /** @var array<class-string|string, list<mixed>> */
21
    private array $cachedDependencies = [];
22
23
    /** @var array<class-string, object> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, object> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, object>.
Loading history...
24
    private array $singletonInstances = [];
25
26
    /** @var array<string, bool> Cache for attribute existence checks */
27
    private array $attributeCache = [];
28
29
    private ?DependencyResolver $dependencyResolver = null;
30
31
    /**
32
     * @param array<class-string, class-string|callable|object> $bindings
33
     * @param array<string, array<class-string, class-string|callable|object>> $contextualBindings
34
     */
35 89
    public function __construct(
36
        private array $bindings = [],
37
        private array &$contextualBindings = [],
38
    ) {
39 89
    }
40
41
    /**
42
     * Resolve dependencies for a class, using cache if available.
43
     *
44
     * @param class-string $className
45
     *
46
     * @return list<mixed>
47
     */
48
    public function resolveDependencies(string $className): array
49
    {
50
        if (!isset($this->cachedDependencies[$className])) {
51
            $this->cachedDependencies[$className] = $this
52
                ->getDependencyResolver()
53
                ->resolveDependencies($className);
54
        }
55
56
        return $this->cachedDependencies[$className];
57
    }
58
59
    /**
60
     * Resolve dependencies for a callable with a specific cache key.
61
     *
62
     * @return list<mixed>
63
     */
64 5
    public function resolveCallableDependencies(string $callableKey, Closure $callable): array
65
    {
66 5
        if (!isset($this->cachedDependencies[$callableKey])) {
67 5
            $this->cachedDependencies[$callableKey] = $this
68 5
                ->getDependencyResolver()
69 5
                ->resolveDependencies($callable);
70
        }
71
72 5
        return $this->cachedDependencies[$callableKey];
73
    }
74
75
    /**
76
     * Pre-warm the dependency cache for multiple classes.
77
     *
78
     * @param list<class-string> $classNames
79
     */
80 3
    public function warmUp(array $classNames): void
81
    {
82 3
        foreach ($classNames as $className) {
83 3
            if (!class_exists($className)) {
84 1
                continue;
85
            }
86
87
            // Pre-resolve dependencies to populate cache
88 3
            if (!isset($this->cachedDependencies[$className])) {
89 3
                $this->cachedDependencies[$className] = $this
90 3
                    ->getDependencyResolver()
91 3
                    ->resolveDependencies($className);
92
            }
93
        }
94
    }
95
96
    /**
97
     * Instantiate a class using cached dependencies.
98
     *
99
     * @param class-string $class
100
     */
101 38
    public function instantiate(string $class): ?object
102
    {
103 38
        if (!class_exists($class)) {
104
            return null;
105
        }
106
107
        // Check for #[Singleton] attribute (cached)
108 38
        if ($this->hasAttribute($class, Singleton::class)) {
109 8
            if (isset($this->singletonInstances[$class])) {
110 8
                return $this->singletonInstances[$class];
111
            }
112
113 8
            $instance = $this->createInstance($class);
114 8
            $this->singletonInstances[$class] = $instance;
115 8
            return $instance;
116
        }
117
118
        // Check for #[Factory] attribute (cached) - always create new instance
119 33
        if ($this->hasAttribute($class, Factory::class)) {
120
            // Don't cache dependencies for factory classes to ensure fresh instances
121 6
            $dependencies = $this
122 6
                ->getDependencyResolver()
123 6
                ->resolveDependencies($class);
124
125
            /** @psalm-suppress MixedMethodCall */
126 6
            return new $class(...$dependencies);
127
        }
128
129
        // Default behavior - create new instance
130 29
        return $this->createInstance($class);
131
    }
132
133
    /**
134
     * Get the number of cached dependency resolutions.
135
     */
136 8
    public function getCacheSize(): int
137
    {
138 8
        return count($this->cachedDependencies);
139
    }
140
141
    /**
142
     * Get all cached class names.
143
     *
144
     * @return list<string>
145
     */
146
    public function getCachedClasses(): array
147
    {
148
        return array_keys($this->cachedDependencies);
149
    }
150
151
    /**
152
     * Create a new instance of a class using cached dependencies.
153
     *
154
     * @param class-string $class
155
     */
156 35
    private function createInstance(string $class): object
157
    {
158 35
        if (!isset($this->cachedDependencies[$class])) {
159 33
            $this->cachedDependencies[$class] = $this
160 33
                ->getDependencyResolver()
161 33
                ->resolveDependencies($class);
162
        }
163
164
        /** @psalm-suppress MixedMethodCall */
165 32
        return new $class(...$this->cachedDependencies[$class]);
166
    }
167
168 44
    private function getDependencyResolver(): DependencyResolver
169
    {
170 44
        if ($this->dependencyResolver === null) {
171 44
            $this->dependencyResolver = new DependencyResolver(
172 44
                $this->bindings,
173 44
                $this->contextualBindings,
174 44
            );
175
        }
176
177 44
        return $this->dependencyResolver;
178
    }
179
180
    /**
181
     * Check if a class has a specific attribute, with caching.
182
     *
183
     * @param class-string $class
184
     * @param class-string $attributeClass
185
     */
186 38
    private function hasAttribute(string $class, string $attributeClass): bool
187
    {
188 38
        $cacheKey = $class . '::' . $attributeClass;
189
190 38
        if (!isset($this->attributeCache[$cacheKey])) {
191 38
            $reflection = new ReflectionClass($class);
192 38
            $this->attributeCache[$cacheKey] = count($reflection->getAttributes($attributeClass)) > 0;
193
        }
194
195 38
        return $this->attributeCache[$cacheKey];
196
    }
197
}
198