163 lines
4.9 KiB
PHP
163 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace Libs;
|
|
|
|
use Closure;
|
|
use Exception;
|
|
use ReflectionClass;
|
|
use ReflectionFunction;
|
|
use ReflectionMethod;
|
|
use ReflectionParameter;
|
|
|
|
/**
|
|
* Synapsis - DuckBrain
|
|
*
|
|
* Class responsible for dependency resolution and injection.
|
|
*
|
|
* @author KJ
|
|
* @website https://kj2.me
|
|
* @license MIT
|
|
*/
|
|
class Synapsis
|
|
{
|
|
/**
|
|
* @var array<class-string, class-string> $bindings
|
|
*/
|
|
private static array $bindings = [];
|
|
/**
|
|
* @var array<class-string, mixed> $instances
|
|
*/
|
|
private static array $instances = [];
|
|
|
|
/**
|
|
* Binds an interface to a class to instantiate it instead.
|
|
*
|
|
* @param class-string $interfaceName
|
|
* @param class-string $className
|
|
*
|
|
* @return void
|
|
* @throws Exception If the interface or class does not exist.
|
|
*/
|
|
public static function bind(string $interfaceName, string $className): void
|
|
{
|
|
if (interface_exists($interfaceName) && class_exists($className)) {
|
|
static::$bindings[$interfaceName] = $className;
|
|
return;
|
|
}
|
|
|
|
throw new Exception('Error at binding non existant Interface or ClassName.');
|
|
}
|
|
|
|
/**
|
|
* Binds multiple interfaces to multiple classes in bulk.
|
|
*
|
|
* @param array<class-string, class-string> $bindings
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function bulkBind(array $bindings): void
|
|
{
|
|
foreach ($bindings as $interfaceName => $className) {
|
|
static::bind($interfaceName, $className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves and injects dependencies for a callable and returns its result.
|
|
*
|
|
* @param callable $action
|
|
*
|
|
* @return mixed
|
|
* @throws Exception If an unhandled callable type is provided.
|
|
*/
|
|
public static function resolve(callable $action): mixed
|
|
{
|
|
if ($action instanceof Closure) { // If it's an anonymous function
|
|
$reflectionCallback = new ReflectionFunction($action);
|
|
} else { // Array or string with the method
|
|
if (is_string($action)) {
|
|
$action = preg_split('/::/', $action);
|
|
}
|
|
|
|
// Check if it's a method or just a function
|
|
if (count($action) == 2) {
|
|
$reflectionCallback = new ReflectionMethod($action[0], $action[1]);
|
|
} else {
|
|
$reflectionCallback = new ReflectionFunction($action[0]);
|
|
}
|
|
}
|
|
|
|
// Get the parameters
|
|
return call_user_func_array(
|
|
$action,
|
|
static::resolveParameterValues($reflectionCallback->getParameters())
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolves and injects dependencies for a class and returns its result.
|
|
*
|
|
* @param class-string $className
|
|
*
|
|
* @return mixed
|
|
* @throws Exception If a binding is missing for an interface or the class does not exist.
|
|
*/
|
|
public static function &resolveInstance(string $className): mixed
|
|
{
|
|
if (interface_exists($className)) {
|
|
if (isset(static::$bindings[$className])) {
|
|
$className = static::$bindings[$className];
|
|
} else {
|
|
throw new Exception("Missing binding for interface: {$className}");
|
|
}
|
|
}
|
|
|
|
if (!class_exists($className)) {
|
|
throw new Exception("Cannot resolve: {$className}");
|
|
}
|
|
|
|
if (!isset(static::$instances[$className])) {
|
|
// If the instance has not been resolved before, resolve it
|
|
$reflectionClass = new ReflectionClass($className);
|
|
$constructor = $reflectionClass->getConstructor();
|
|
static::$instances[$className] = new $className(
|
|
...static::resolveParameterValues($constructor?->getParameters() ?? [])
|
|
);
|
|
}
|
|
|
|
return static::$instances[$className];
|
|
}
|
|
|
|
/**
|
|
* Resolves parameter values by injecting dependencies.
|
|
*
|
|
* @param array<ReflectionParameter> $parameters
|
|
*
|
|
* @return array<mixed>
|
|
* @throws Exception If a primitive parameter does not have a default value.
|
|
*/
|
|
public static function resolveParameterValues(array $parameters): array
|
|
{
|
|
$values = [];
|
|
foreach ($parameters as $parameter) {
|
|
if ($parameter->isOptional()) { // Always use the default value first
|
|
$values[] = $parameter->getDefaultValue();
|
|
continue;
|
|
}
|
|
|
|
$paramType = $parameter->getType();
|
|
if ($paramType === null) {
|
|
// If no type is declared, and it's not optional, we cannot resolve it.
|
|
throw new Exception('Untyped parameters expect at least a default value or must be optional.');
|
|
}
|
|
|
|
if ($paramType->isBuiltin()) {
|
|
throw new Exception("Primitive parameter '{$parameter->getName()}' expects at least a default value.");
|
|
}
|
|
|
|
$values[] = static::resolveInstance($paramType->getName());
|
|
}
|
|
return $values;
|
|
}
|
|
}
|