<?php
namespace Framework;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
/**
* Dependency Injection Container with autowiring support.
*
* Features:
* - Factory registration for lazy instantiation
* - Autowiring for classes with type-hinted constructors
* - Singleton caching of resolved services
*/
final class Container
{
/** @var array<string, object> Cached singleton instances */
private array $instances = [];
/** @var array<string, callable> 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<T>|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());
}
}