Cached singleton instances */ private array $instances = []; /** @var array Factory functions */ private array $factories = []; /** * Register a factory for a service. * * @param string $id Service identifier (usually class name) * @param callable(Container): object $factory Factory function */ public function set(string $id, callable $factory): void { $this->factories[$id] = $factory; // Clear cached instance if re-registering unset($this->instances[$id]); } /** * Register an existing instance as a singleton. * * @param string $id Service identifier * @param object $instance The instance to register */ public function instance(string $id, object $instance): void { $this->instances[$id] = $instance; } /** * Check if a service is registered. */ public function has(string $id): bool { return isset($this->instances[$id]) || isset($this->factories[$id]) || class_exists($id); } /** * Get a service by ID. * * Resolution order: * 1. Check cached instances * 2. Check registered factories * 3. Try autowiring if ID is a class name * * @template T of object * @param class-string|string $id * @return T|object * @throws ContainerException */ public function get(string $id): object { // Return cached instance if available if (isset($this->instances[$id])) { return $this->instances[$id]; } // Use factory if registered if (isset($this->factories[$id])) { $this->instances[$id] = ($this->factories[$id])($this); return $this->instances[$id]; } // Try autowiring for class names if (class_exists($id)) { $this->instances[$id] = $this->autowire($id); return $this->instances[$id]; } throw new ContainerException("Service not found: {$id}"); } /** * Autowire a class by resolving constructor dependencies. * * @param class-string $class * @throws ContainerException */ private function autowire(string $class): object { $reflection = new ReflectionClass($class); if (!$reflection->isInstantiable()) { throw new ContainerException("Cannot instantiate {$class}"); } $constructor = $reflection->getConstructor(); // No constructor = no dependencies if ($constructor === null) { return new $class(); } $parameters = $constructor->getParameters(); $dependencies = []; foreach ($parameters as $param) { $dependencies[] = $this->resolveParameter($param, $class); } return $reflection->newInstanceArgs($dependencies); } /** * Resolve a single constructor parameter. * * @throws ContainerException */ private function resolveParameter(ReflectionParameter $param, string $class): mixed { $type = $param->getType(); // Handle nullable/optional parameters with default values if ($param->isDefaultValueAvailable()) { // Try to resolve the type, but fall back to default if it fails if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { $typeName = $type->getName(); // Only try to resolve if explicitly registered (not just class_exists) if (isset($this->instances[$typeName]) || isset($this->factories[$typeName])) { try { return $this->get($typeName); } catch (ContainerException) { // Fall through to default value } } } return $param->getDefaultValue(); } // No type hint if ($type === null) { throw new ContainerException( "Cannot resolve parameter \${$param->getName()} in {$class}: no type hint" ); } // Union types not supported if (!$type instanceof ReflectionNamedType) { throw new ContainerException( "Cannot resolve parameter \${$param->getName()} in {$class}: union types not supported" ); } // Built-in types (string, int, etc.) cannot be autowired if ($type->isBuiltin()) { if ($type->allowsNull()) { return null; } throw new ContainerException( "Cannot resolve parameter \${$param->getName()} in {$class}: built-in type {$type->getName()}" ); } // Resolve the dependency return $this->get($type->getName()); } }