gacela-project /
container
| 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
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
|
|||
| 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 |