feat(di): implement attribute resolver system for dependency injection

- Introduce `ParameterAttributeResolverInterface` for handling attribute-based parameter resolution.
- Add `EnvAttributeResolver` to inject environment variables with type conversion.
- Add `LogChannelAttributeResolver` to inject channel-specific loggers.
- Create `ParameterAttributeResolverRegistry` to manage available resolvers.
- Update `ParameterResolver` to delegate attribute resolution to the registry.
- Add comprehensive unit tests for all attribute resolvers and registry functionality.
This commit is contained in:
2025-11-03 21:00:04 +01:00
parent 9f0dfd131a
commit 1655248de5
10 changed files with 744 additions and 36 deletions

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use Attribute;
#[Attribute(Attribute::TARGET_PARAMETER|Attribute::IS_REPEATABLE)]
final readonly class Env
{
public function __construct(
public EnvKey $key,
) {}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Attributes;
use App\Framework\Config\Env as EnvAttribute;
use App\Framework\Config\Environment;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
/**
* Resolver für #[Env(EnvKey::...)] Attribut
*
* Injiziert Environment-Variablen basierend auf dem EnvKey.
* Führt automatische Type-Konvertierung basierend auf dem Parameter-Typ durch.
*
* Beispiel:
* ```php
* public function __construct(
* #[Env(EnvKey::APP_NAME)] private string $appName
* ) {}
* ```
*/
final readonly class EnvAttributeResolver implements ParameterAttributeResolverInterface
{
public function __construct(
private Container $container
) {
}
public function supports(\ReflectionAttribute $attribute): bool
{
return $attribute->getName() === EnvAttribute::class;
}
public function resolve(
\ReflectionParameter $param,
ClassName $className,
string $methodName
): mixed {
$attributes = $param->getAttributes(EnvAttribute::class);
if (empty($attributes)) {
return null;
}
/** @var EnvAttribute $envAttr */
$envAttr = $attributes[0]->newInstance();
$environment = $this->container->get(Environment::class);
$type = $param->getType();
// Type-Konvertierung basierend auf Parameter-Typ
if ($type instanceof \ReflectionNamedType) {
$typeName = $type->getName();
$default = $this->getDefaultForType($typeName);
return match ($typeName) {
'string' => $environment->getString($envAttr->key, $default),
'int' => $environment->getInt($envAttr->key, $default),
'bool' => $environment->getBool($envAttr->key, $default),
'float' => $environment->getFloat($envAttr->key, $default),
default => $environment->get($envAttr->key, $default),
};
}
// Fallback für Union Types oder andere komplexe Typen
return $environment->get($envAttr->key);
}
/**
* Gibt den Default-Wert für einen Typ zurück
*/
private function getDefaultForType(string $typeName): mixed
{
return match ($typeName) {
'string' => '',
'int' => 0,
'bool' => false,
'float' => 0.0,
default => null,
};
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Attributes;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
use App\Framework\Logging\Attributes\LogChannel as LogChannelAttribute;
use App\Framework\Logging\SupportsChannels;
/**
* Resolver für #[LogChannel] Attribut
*
* Injiziert Channel-spezifische Logger basierend auf dem LogChannel Attribut.
*/
final readonly class LogChannelAttributeResolver implements ParameterAttributeResolverInterface
{
public function __construct(
private Container $container
) {
}
public function supports(\ReflectionAttribute $attribute): bool
{
return $attribute->getName() === LogChannelAttribute::class;
}
public function resolve(
\ReflectionParameter $param,
ClassName $className,
string $methodName
): mixed {
$attributes = $param->getAttributes(LogChannelAttribute::class);
if (empty($attributes)) {
return null;
}
/** @var LogChannelAttribute $logChannelAttr */
$logChannelAttr = $attributes[0]->newInstance();
// Haupt-Logger vom Container holen (SupportsChannels Interface)
$mainLogger = $this->container->get(SupportsChannels::class);
// Channel-Logger zurückgeben (Logger & HasChannel)
return $mainLogger->channel($logChannelAttr->channel);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Attributes;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Interface für Parameter-Attribut-Resolver
*
* Jeder Resolver kann spezifische Attribute behandeln und
* Parameter-Werte basierend auf diesen Attributen auflösen.
*/
interface ParameterAttributeResolverInterface
{
/**
* Prüft ob dieser Resolver das gegebene Attribut unterstützt
*
* @param \ReflectionAttribute $attribute Das zu prüfende Attribut
* @return bool True wenn dieser Resolver das Attribut behandeln kann
*/
public function supports(\ReflectionAttribute $attribute): bool;
/**
* Löst einen Parameter-Wert basierend auf dem Attribut auf
*
* @param \ReflectionParameter $param Der Parameter mit dem Attribut
* @param ClassName $className Die Klasse, zu der der Parameter gehört
* @param string $methodName Der Methodenname, zu der der Parameter gehört
* @return mixed|null Der aufgelöste Wert oder null wenn nicht auflösbar
*/
public function resolve(
\ReflectionParameter $param,
ClassName $className,
string $methodName
): mixed;
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Attributes;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Registry für Parameter-Attribut-Resolver
*
* Verwaltet eine Sammlung von Resolvern und findet den passenden
* Resolver für ein gegebenes Attribut.
*
* Verwendet variadic Constructor für einfache Registrierung:
* ```php
* $registry = new ParameterAttributeResolverRegistry(
* new LogChannelAttributeResolver($container),
* new EnvAttributeResolver($container)
* );
* ```
*/
final readonly class ParameterAttributeResolverRegistry
{
/**
* @param ParameterAttributeResolverInterface ...$resolvers
*/
public function __construct(
ParameterAttributeResolverInterface ...$resolvers
) {
$this->resolvers = $resolvers;
}
/**
* @var ParameterAttributeResolverInterface[]
*/
private readonly array $resolvers;
/**
* Löst einen Parameter-Wert basierend auf seinen Attributen auf
*
* Iteriert über alle Attribute des Parameters und versucht,
* einen passenden Resolver zu finden. Der erste Resolver, der
* ein Attribut unterstützt, wird verwendet.
*
* @param \ReflectionParameter $param Der Parameter mit möglichen Attributen
* @param ClassName $className Die Klasse, zu der der Parameter gehört
* @param string $methodName Der Methodenname, zu der der Parameter gehört
* @return mixed|null Der aufgelöste Wert oder null wenn kein Resolver passt
*/
public function resolve(
\ReflectionParameter $param,
ClassName $className,
string $methodName
): mixed {
$attributes = $param->getAttributes();
foreach ($attributes as $attribute) {
foreach ($this->resolvers as $resolver) {
if ($resolver->supports($attribute)) {
return $resolver->resolve($param, $className, $methodName);
}
}
}
return null;
}
}

View File

@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Logging\Attributes\LogChannel;
use App\Framework\Logging\SupportsChannels;
use App\Framework\DI\Attributes\EnvAttributeResolver;
use App\Framework\DI\Attributes\LogChannelAttributeResolver;
use App\Framework\DI\Attributes\ParameterAttributeResolverRegistry;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Reflection\WrappedReflectionParameter;
@@ -19,10 +20,18 @@ use App\Framework\Reflection\WrappedReflectionParameter;
*/
final readonly class ParameterResolver
{
private readonly ParameterAttributeResolverRegistry $attributeResolverRegistry;
public function __construct(
private Container $container,
private ReflectionProvider $reflectionProvider
private ReflectionProvider $reflectionProvider,
?ParameterAttributeResolverRegistry $attributeResolverRegistry = null
) {
// Erstelle Standard-Registry mit Default-Resolvern, wenn nicht angegeben
$this->attributeResolverRegistry = $attributeResolverRegistry ?? new ParameterAttributeResolverRegistry(
new LogChannelAttributeResolver($container),
new EnvAttributeResolver($container)
);
}
/**
@@ -33,7 +42,8 @@ final readonly class ParameterResolver
* 2. Attribute-basierte Injection (#[LogChannel], etc.)
* 3. Type-basierte Injection vom Container
* 4. Default-Werte
* 5. Nullable Parameter (null)
* 5. Nullable Interfaces/Classes: Versuche explizit zu resolven, dann null
* 6. Nullable Parameter (builtin types wie ?string, etc.) → null
*
* @param array<string, mixed> $overrides Named parameter overrides
* @return array<int, mixed> Resolved parameter values
@@ -86,7 +96,17 @@ final readonly class ParameterResolver
return $param->getDefaultValue();
}
// 5. Nullable Parameter
// 5. Nullable Interfaces/Classes: Versuche explizit zu resolven, bevor null zurückgegeben wird
if ($this->isNullableNonBuiltinType($param)) {
$typeName = $param->getTypeName();
if ($typeName !== null && $this->container->has($typeName)) {
return $this->container->get($typeName);
}
// Wenn nicht resolvbar, null zurückgeben
return null;
}
// 6. Nullable Parameter (builtin types wie ?string, etc.)
if ($param->allowsNull()) {
return null;
}
@@ -100,48 +120,23 @@ final readonly class ParameterResolver
/**
* Versucht Parameter über Attributes aufzulösen
*
* Nutzt direkt die native Reflection API für maximale Einfachheit
*
* Unterstützte Attributes:
* - #[LogChannel] - Injiziert Channel-spezifischen Logger
* Verwendet die ParameterAttributeResolverRegistry, um verschiedene
* Attribute zu behandeln. Unterstützte Attributes werden über die
* Registry aufgelöst.
*/
private function resolveFromAttributes(
WrappedReflectionParameter $param,
ClassName $className,
string $methodName
): mixed {
// Direkt native Reflection API nutzen
// Direkt native Reflection API nutzen für Attribute-Zugriff
$nativeParam = new \ReflectionParameter(
[$className->getFullyQualified(), $methodName],
$param->getPosition()
);
// #[LogChannel] Attribut
$logChannelAttrs = $nativeParam->getAttributes(LogChannel::class);
if (!empty($logChannelAttrs)) {
/** @var LogChannel $logChannelAttr */
$logChannelAttr = $logChannelAttrs[0]->newInstance();
return $this->resolveLogChannelAttribute($logChannelAttr);
}
// Weitere Attributes können hier hinzugefügt werden
// z.B. #[Inject], #[Autowire], #[Config], etc.
return null;
}
/**
* Löst #[LogChannel] Attribut auf
*
* Injiziert einen Channel-spezifischen Logger statt des Haupt-Loggers
*/
private function resolveLogChannelAttribute(LogChannel $attribute): mixed
{
// Haupt-Logger vom Container holen (SupportsChannels Interface)
$mainLogger = $this->container->get(SupportsChannels::class);
// Channel-Logger zurückgeben (Logger & HasChannel)
return $mainLogger->channel($attribute->channel);
// Verwende Registry, um Attribute aufzulösen
return $this->attributeResolverRegistry->resolve($nativeParam, $className, $methodName);
}
/**
@@ -161,4 +156,21 @@ final readonly class ParameterResolver
return null;
}
/**
* Prüft ob ein Parameter nullable ist und ein Interface/Class (nicht builtin) ist
*/
private function isNullableNonBuiltinType(WrappedReflectionParameter $param): bool
{
if (!$param->allowsNull()) {
return false;
}
$typeName = $param->getTypeName();
if ($typeName === null || $param->isBuiltin()) {
return false;
}
return true;
}
}