70b1016refactor(autoload): remove global exception handlera534613refactor(request): propagate validation failures as exceptions5f3fc16feat(router): add error boundary with exception callback9dbfd7dfeat: add named parameter injection to resolve()
180 lines
5.8 KiB
PHP
180 lines
5.8 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
|
|
* @param array<string, mixed> $named Associative array of values injected into the callable's
|
|
* parameters by exact name match. Consumed in resolveParameterValues().
|
|
*
|
|
* @return mixed
|
|
* @throws Exception If an unhandled callable type is provided.
|
|
*/
|
|
public static function resolve(callable $action, array $named = []): 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(), $named)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param array<string, mixed> $named
|
|
* Values injected into parameters whose name matches the key,
|
|
* taking precedence over optional defaults and DI resolution.
|
|
* Keys matching no parameter are ignored.
|
|
*
|
|
* @return array<mixed>
|
|
* @throws Exception If a primitive parameter does not have a default value.
|
|
*/
|
|
public static function resolveParameterValues(array $parameters, array $named = []): array
|
|
{
|
|
$values = [];
|
|
foreach ($parameters as $parameter) {
|
|
if ($parameter->isVariadic()) { // Skip variatic parameters
|
|
continue;
|
|
}
|
|
|
|
if (array_key_exists($parameter->getName(), $named)) { // Named values win over defaults and DI
|
|
$values[] = $named[$parameter->getName()];
|
|
continue;
|
|
}
|
|
|
|
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 parameter '{$parameter}' 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;
|
|
}
|
|
}
|