Files
duckbrain/src/Libs/Synapsis.php
kj eb882db0c8 sync: 2026-09-07
70b1016 refactor(autoload): remove global exception handler
a534613 refactor(request): propagate validation failures as exceptions
5f3fc16 feat(router): add error boundary with exception callback
9dbfd7d feat: add named parameter injection to resolve()
2026-09-07 18:00:18 -03:00

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