134 lines
4.3 KiB
PHP
134 lines
4.3 KiB
PHP
<?php
|
|
namespace Core;
|
|
|
|
/**
|
|
* Simple service container / dependency injector.
|
|
*
|
|
* - bind($abstract, $concrete): registra una factory, callable o nome classe.
|
|
* - singleton($abstract, $concrete): come bind ma mantiene l'istanza.
|
|
* - make($abstract): risolve e restituisce l'istanza.
|
|
*
|
|
* Nota: usa reflection per risolvere le dipendenze del costruttore.
|
|
*/
|
|
class Container {
|
|
/** @var array<string,mixed> */
|
|
private $bindings = [];
|
|
|
|
/** @var array<string,mixed> */
|
|
private $instances = [];
|
|
|
|
/**
|
|
* Bind an abstract name (interface or key) to a concrete implementation.
|
|
* $concrete can be:
|
|
* - a callable: function(Container $c) { return new Foo(); }
|
|
* - a string class name: 'App\\Foo'
|
|
*
|
|
* @param string $abstract
|
|
* @param callable|string $concrete
|
|
* @return void
|
|
*/
|
|
public function bind(string $abstract, $concrete): void {
|
|
$this->bindings[$abstract] = $concrete;
|
|
}
|
|
|
|
/**
|
|
* Bind as singleton. The first resolved instance is cached and returned afterwards.
|
|
*
|
|
* @param string $abstract
|
|
* @param callable|string $concrete
|
|
* @return void
|
|
*/
|
|
public function singleton(string $abstract, $concrete): void {
|
|
$this->bindings[$abstract] = $concrete;
|
|
// mark as singleton with null placeholder
|
|
$this->instances[$abstract] = null;
|
|
}
|
|
|
|
/**
|
|
* Resolve an abstract to an instance.
|
|
*
|
|
* @param string $abstract
|
|
* @return mixed
|
|
* @throws \Exception
|
|
*/
|
|
public function make(string $abstract) {
|
|
// return existing singleton instance if already created
|
|
if (array_key_exists($abstract, $this->instances) && $this->instances[$abstract] !== null) {
|
|
return $this->instances[$abstract];
|
|
}
|
|
|
|
if (!isset($this->bindings[$abstract])) {
|
|
// if no binding, try to instantiate the abstract directly if it's a class
|
|
if (class_exists($abstract)) {
|
|
$object = $this->build($abstract);
|
|
} else {
|
|
throw new \Exception("No binding found for [{$abstract}]");
|
|
}
|
|
} else {
|
|
$concrete = $this->bindings[$abstract];
|
|
|
|
if (is_callable($concrete)) {
|
|
// factory receives the container
|
|
$object = $concrete($this);
|
|
} elseif (is_string($concrete) && class_exists($concrete)) {
|
|
$object = $this->build($concrete);
|
|
} else {
|
|
throw new \Exception("Invalid binding for [{$abstract}]");
|
|
}
|
|
}
|
|
|
|
// if abstract was registered as singleton, cache the instance
|
|
if (array_key_exists($abstract, $this->instances)) {
|
|
$this->instances[$abstract] = $object;
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* Build an instance of the given class resolving constructor dependencies.
|
|
*
|
|
* @param string $class
|
|
* @return object
|
|
* @throws \Exception
|
|
*/
|
|
private function build(string $class) {
|
|
$reflector = new \ReflectionClass($class);
|
|
|
|
if (!$reflector->isInstantiable()) {
|
|
throw new \Exception("Class {$class} is not instantiable");
|
|
}
|
|
|
|
$constructor = $reflector->getConstructor();
|
|
if (is_null($constructor)) {
|
|
return new $class();
|
|
}
|
|
|
|
$params = $constructor->getParameters();
|
|
$dependencies = [];
|
|
|
|
foreach ($params as $param) {
|
|
$type = $param->getType();
|
|
|
|
// If parameter has a class/interface type, resolve it from container
|
|
if ($type && !$type->isBuiltin()) {
|
|
$depClass = $type->getName();
|
|
$dependencies[] = $this->make($depClass);
|
|
continue;
|
|
}
|
|
|
|
// If default value is available, use it
|
|
if ($param->isDefaultValueAvailable()) {
|
|
$dependencies[] = $param->getDefaultValue();
|
|
continue;
|
|
}
|
|
|
|
// Cannot resolve the dependency
|
|
$name = $param->getName();
|
|
throw new \Exception("Unresolvable dependency [\${$name}] in class {$class}");
|
|
}
|
|
|
|
return $reflector->newInstanceArgs($dependencies);
|
|
}
|
|
}
|