Files
elixforms-web-services/src/Core/Container.php
T

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);
}
}