Passed
Pull Request — main (#20)
by Chema
01:55
created

DependencyCacheManager::getDependencyResolver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 2
rs 10
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
    private ?DependencyResolver $dependencyResolver = null;
27
28
    /**
29
     * @param array<class-string, class-string|callable|object> $bindings
30
     * @param array<string, array<class-string, class-string|callable|object>> $contextualBindings
31
     */
32 82
    public function __construct(
33
        private array $bindings = [],
34
        private array &$contextualBindings = [],
35
    ) {
36 82
    }
37
38
    /**
39
     * Resolve dependencies for a class, using cache if available.
40
     *
41
     * @param class-string $className
42
     *
43
     * @return list<mixed>
44
     */
45
    public function resolveDependencies(string $className): array
46
    {
47
        if (!isset($this->cachedDependencies[$className])) {
48
            $this->cachedDependencies[$className] = $this
49
                ->getDependencyResolver()
50
                ->resolveDependencies($className);
51
        }
52
53
        return $this->cachedDependencies[$className];
54
    }
55
56
    /**
57
     * Resolve dependencies for a callable with a specific cache key.
58
     *
59
     * @return list<mixed>
60
     */
61 5
    public function resolveCallableDependencies(string $callableKey, Closure $callable): array
62
    {
63 5
        if (!isset($this->cachedDependencies[$callableKey])) {
64 5
            $this->cachedDependencies[$callableKey] = $this
65 5
                ->getDependencyResolver()
66 5
                ->resolveDependencies($callable);
67
        }
68
69 5
        return $this->cachedDependencies[$callableKey];
70
    }
71
72
    /**
73
     * Pre-warm the dependency cache for multiple classes.
74
     *
75
     * @param list<class-string> $classNames
76
     */
77 3
    public function warmUp(array $classNames): void
78
    {
79 3
        foreach ($classNames as $className) {
80 3
            if (!class_exists($className)) {
81 1
                continue;
82
            }
83
84
            // Pre-resolve dependencies to populate cache
85 3
            if (!isset($this->cachedDependencies[$className])) {
86 3
                $this->cachedDependencies[$className] = $this
87 3
                    ->getDependencyResolver()
88 3
                    ->resolveDependencies($className);
89
            }
90
        }
91
    }
92
93
    /**
94
     * Instantiate a class using cached dependencies.
95
     *
96
     * @param class-string $class
97
     */
98 31
    public function instantiate(string $class): ?object
99
    {
100 31
        if (!class_exists($class)) {
101
            return null;
102
        }
103
104 31
        $reflection = new ReflectionClass($class);
105
106
        // Check for #[Singleton] attribute
107 31
        $singletonAttributes = $reflection->getAttributes(Singleton::class);
108 31
        if (count($singletonAttributes) > 0) {
109 3
            if (isset($this->singletonInstances[$class])) {
110 3
                return $this->singletonInstances[$class];
111
            }
112
113 3
            $instance = $this->createInstance($class);
114 3
            $this->singletonInstances[$class] = $instance;
115 3
            return $instance;
116
        }
117
118
        // Check for #[Factory] attribute - always create new instance
119 29
        $factoryAttributes = $reflection->getAttributes(Factory::class);
120 29
        if (count($factoryAttributes) > 0) {
121
            // Don't cache dependencies for factory classes to ensure fresh instances
122 3
            $dependencies = $this
123 3
                ->getDependencyResolver()
124 3
                ->resolveDependencies($class);
125
126
            /** @psalm-suppress MixedMethodCall */
127 3
            return new $class(...$dependencies);
128
        }
129
130
        // Default behavior - create new instance
131 27
        return $this->createInstance($class);
132
    }
133
134
    /**
135
     * Get the number of cached dependency resolutions.
136
     */
137 8
    public function getCacheSize(): int
138
    {
139 8
        return count($this->cachedDependencies);
140
    }
141
142
    /**
143
     * Get all cached class names.
144
     *
145
     * @return list<string>
146
     */
147
    public function getCachedClasses(): array
148
    {
149
        return array_keys($this->cachedDependencies);
150
    }
151
152
    /**
153
     * Create a new instance of a class using cached dependencies.
154
     *
155
     * @param class-string $class
156
     */
157 29
    private function createInstance(string $class): object
158
    {
159 29
        if (!isset($this->cachedDependencies[$class])) {
160 27
            $this->cachedDependencies[$class] = $this
161 27
                ->getDependencyResolver()
162 27
                ->resolveDependencies($class);
163
        }
164
165
        /** @psalm-suppress MixedMethodCall */
166 26
        return new $class(...$this->cachedDependencies[$class]);
167
    }
168
169 37
    private function getDependencyResolver(): DependencyResolver
170
    {
171 37
        if ($this->dependencyResolver === null) {
172 37
            $this->dependencyResolver = new DependencyResolver(
173 37
                $this->bindings,
174 37
                $this->contextualBindings,
175 37
            );
176
        }
177
178 37
        return $this->dependencyResolver;
179
    }
180
}
181