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:
14
src/Framework/Config/Env.php
Normal file
14
src/Framework/Config/Env.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
85
src/Framework/DI/Attributes/EnvAttributeResolver.php
Normal file
85
src/Framework/DI/Attributes/EnvAttributeResolver.php
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
49
src/Framework/DI/Attributes/LogChannelAttributeResolver.php
Normal file
49
src/Framework/DI/Attributes/LogChannelAttributeResolver.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user