foundation/src/Core/DependencyInjection/InflectableContainer.php
2025-06-13 18:29:55 +02:00

195 lines
No EOL
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Foundation\Core\DependencyInjection;
use DI\Container;
use Psr\Container\ContainerInterface;
use function DI\create;
class InflectableContainer implements ContainerInterface
{
private array $inflections = [];
/** @var AttributeProcessorInterface[] */
private array $attributeProcessors = [];
public function __construct(private readonly Container $phpDiContainer)
{
}
/**
* @template T of object
* @param string $id
* @return ($id is class-string<T> ? T : mixed)
*/
public function get(string $id): mixed
{
$instance = $this->phpDiContainer->get($id);
// Only apply inflections to objects, not arrays or primitives
if (is_object($instance)) {
return $this->applyInflections($instance);
}
return $instance;
}
public function has(string $id): bool
{
return $this->phpDiContainer->has($id);
}
public function set(string $name, mixed $value): void
{
$this->phpDiContainer->set($name, $value);
}
/**
* @template T of object
* @param class-string<T> $name
* @param array $parameters
* @return T
*/
public function make(string $name, array $parameters = []): object
{
$instance = $this->phpDiContainer->make($name, $parameters);
return $this->applyInflections($instance);
}
public function inflect(string $class): InflectionHelper
{
return new InflectionHelper($this, $class);
}
public function register(string $abstract, array $dependencies = []): object
{
if ($dependencies === []) {
// Simple registration with no dependencies
$this->phpDiContainer->set($abstract, create($abstract));
} else {
// Registration with explicit dependencies
$this->phpDiContainer->set(
$abstract,
function (InflectableContainer $container) use ($abstract, $dependencies): object {
$resolvedDependencies = [];
foreach ($dependencies as $dependency) {
$resolvedDependencies[] = $container->get($dependency);
}
return new $abstract(...$resolvedDependencies);
},
);
}
// Process attributes for the registered class
$this->processAttributes($abstract);
return $this->get($abstract);
}
public function bind(string $abstract, string $concrete, array $dependencies = []): object
{
if ($dependencies === []) {
// Simple interface-to-implementation binding
$this->phpDiContainer->set($abstract, create($concrete));
} else {
// Interface-to-implementation binding with explicit dependencies
$this->phpDiContainer->set(
$abstract,
function (InflectableContainer $container) use ($concrete, $dependencies): object {
$resolvedDependencies = [];
foreach ($dependencies as $dependency) {
$resolvedDependencies[] = $container->get($dependency);
}
return new $concrete(...$resolvedDependencies);
},
);
}
return $this->get($abstract);
}
public function addAttributeProcessor(AttributeProcessorInterface $processor): void
{
$this->attributeProcessors[] = $processor;
}
public function registerInflection(string $targetClass, string $method, array $params): void
{
$this->inflections[$targetClass][] = [
'method' => $method,
'params' => $params,
];
}
private function applyInflections(object $instance): object
{
$instanceClass = $instance::class;
// Apply direct class inflections
$this->applyDirectInflections($instance, $instanceClass);
// Apply interface/parent class inflections
$this->applyInheritanceInflections($instance);
return $instance;
}
private function applyDirectInflections(object $instance, string $instanceClass): void
{
if (isset($this->inflections[$instanceClass])) {
foreach ($this->inflections[$instanceClass] as $inflection) {
$resolvedParams = $this->resolveParameters($inflection['params']);
$methodName = $inflection['method'];
// @phpstan-ignore-next-line Dynamic method call is intentional for inflection
$instance->$methodName(...$resolvedParams);
}
}
}
private function applyInheritanceInflections(object $instance): void
{
foreach ($this->inflections as $targetClass => $inflections) {
if ($instance instanceof $targetClass) {
foreach ($inflections as $inflection) {
$resolvedParams = $this->resolveParameters($inflection['params']);
$methodName = $inflection['method'];
// @phpstan-ignore-next-line Dynamic method call is intentional for inflection
$instance->$methodName(...$resolvedParams);
}
}
}
}
private function resolveParameters(array $params): array
{
$resolved = [];
foreach ($params as $param) {
if (is_string($param) && $this->isResolvableClass($param)) {
$resolved[] = $this->phpDiContainer->get($param);
} else {
$resolved[] = $param;
}
}
return $resolved;
}
private function processAttributes(string $className): void
{
foreach ($this->attributeProcessors as $processor) {
if ($processor->canProcess($className)) {
$processor->process($className);
}
}
}
private function isResolvableClass(string $param): bool
{
// Check if it's a class name (contains backslash or ends with ::class pattern)
return class_exists($param) || interface_exists($param) || $this->phpDiContainer->has($param);
}
}