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;
|
||||
}
|
||||
}
|
||||
|
||||
161
tests/Unit/Framework/DI/Attributes/EnvAttributeResolverTest.php
Normal file
161
tests/Unit/Framework/DI/Attributes/EnvAttributeResolverTest.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Framework\DI\Attributes;
|
||||
|
||||
use App\Framework\Config\Env as EnvAttribute;
|
||||
use App\Framework\Config\EnvKey;
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\DI\Attributes\EnvAttributeResolver;
|
||||
use App\Framework\DI\Container;
|
||||
|
||||
// Test classes
|
||||
final class ServiceWithEnvString
|
||||
{
|
||||
public function __construct(
|
||||
#[EnvAttribute(EnvKey::APP_NAME)]
|
||||
public string $appName
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithEnvInt
|
||||
{
|
||||
public function __construct(
|
||||
#[EnvAttribute(EnvKey::DB_PORT)]
|
||||
public int $port
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithEnvBool
|
||||
{
|
||||
public function __construct(
|
||||
#[EnvAttribute(EnvKey::APP_DEBUG)]
|
||||
public bool $debug
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithEnvFloat
|
||||
{
|
||||
public function __construct(
|
||||
#[EnvAttribute(EnvKey::APP_NAME)]
|
||||
public float $value
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = $this->createMock(Container::class);
|
||||
$this->environment = $this->createMock(Environment::class);
|
||||
$this->resolver = new EnvAttributeResolver($this->container);
|
||||
|
||||
$this->container->method('get')
|
||||
->with(Environment::class)
|
||||
->willReturn($this->environment);
|
||||
});
|
||||
|
||||
describe('EnvAttributeResolver', function () {
|
||||
it('supports Env attribute', function () {
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvString::class, '__construct'],
|
||||
0
|
||||
);
|
||||
$attributes = $reflectionParam->getAttributes(EnvAttribute::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
expect($this->resolver->supports($attributes[0]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('resolves string type from environment', function () {
|
||||
$this->environment->expects($this->once())
|
||||
->method('getString')
|
||||
->with(EnvKey::APP_NAME, '')
|
||||
->willReturn('My App');
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvString::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvString::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
$result = $this->resolver->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBe('My App');
|
||||
});
|
||||
|
||||
it('resolves int type from environment', function () {
|
||||
$this->environment->expects($this->once())
|
||||
->method('getInt')
|
||||
->with(EnvKey::DB_PORT, 0)
|
||||
->willReturn(5432);
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvInt::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvInt::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
$result = $this->resolver->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBe(5432);
|
||||
expect($result)->toBeInt();
|
||||
});
|
||||
|
||||
it('resolves bool type from environment', function () {
|
||||
$this->environment->expects($this->once())
|
||||
->method('getBool')
|
||||
->with(EnvKey::APP_DEBUG, false)
|
||||
->willReturn(true);
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvBool::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvBool::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
$result = $this->resolver->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
expect($result)->toBeBool();
|
||||
});
|
||||
|
||||
it('resolves float type from environment', function () {
|
||||
$this->environment->expects($this->once())
|
||||
->method('getFloat')
|
||||
->with(EnvKey::APP_NAME, 0.0)
|
||||
->willReturn(123.45);
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvFloat::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvFloat::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
$result = $this->resolver->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBe(123.45);
|
||||
expect($result)->toBeFloat();
|
||||
});
|
||||
|
||||
it('uses generic get for unknown types', function () {
|
||||
$this->environment->expects($this->once())
|
||||
->method('get')
|
||||
->with(EnvKey::APP_NAME, null)
|
||||
->willReturn('some value');
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvString::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithEnvString::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
// Temporär den Typ ändern, um den default-Fall zu testen
|
||||
// (In PHP können wir Reflection nicht direkt ändern, daher testen wir den Fall separat)
|
||||
// Für diesen Test müssen wir einen anderen Ansatz verwenden
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Framework\DI\Attributes;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\DI\Attributes\LogChannelAttributeResolver;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\Logging\Attributes\LogChannel as LogChannelAttribute;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\HasChannel;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogChannel;
|
||||
use App\Framework\Logging\SupportsChannels;
|
||||
|
||||
// Test class
|
||||
final class ServiceWithLogChannel
|
||||
{
|
||||
public function __construct(
|
||||
#[LogChannelAttribute(LogChannel::CACHE)]
|
||||
public Logger $logger
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = $this->createMock(Container::class);
|
||||
$this->resolver = new LogChannelAttributeResolver($this->container);
|
||||
});
|
||||
|
||||
describe('LogChannelAttributeResolver', function () {
|
||||
it('supports LogChannel attribute', function () {
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithLogChannel::class, '__construct'],
|
||||
0
|
||||
);
|
||||
$attributes = $reflectionParam->getAttributes(LogChannelAttribute::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
expect($this->resolver->supports($attributes[0]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not support other attributes', function () {
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithLogChannel::class, '__construct'],
|
||||
0
|
||||
);
|
||||
$attributes = $reflectionParam->getAttributes();
|
||||
|
||||
// Prüfe, ob es ein anderes Attribut gibt (falls vorhanden)
|
||||
foreach ($attributes as $attr) {
|
||||
if ($attr->getName() !== LogChannelAttribute::class) {
|
||||
expect($this->resolver->supports($attr))->toBeFalse();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves LogChannel attribute to channel logger', function () {
|
||||
$mainLogger = new DefaultLogger();
|
||||
|
||||
$this->container->expects($this->once())
|
||||
->method('get')
|
||||
->with(SupportsChannels::class)
|
||||
->willReturn($mainLogger);
|
||||
|
||||
$className = ClassName::create(ServiceWithLogChannel::class);
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithLogChannel::class, '__construct'],
|
||||
0
|
||||
);
|
||||
|
||||
$result = $this->resolver->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBeInstanceOf(Logger::class);
|
||||
expect($result)->toBeInstanceOf(HasChannel::class);
|
||||
expect($result->channel)->toBe(LogChannel::CACHE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Framework\DI\Attributes;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\DI\Attributes\EnvAttributeResolver;
|
||||
use App\Framework\DI\Attributes\LogChannelAttributeResolver;
|
||||
use App\Framework\DI\Attributes\ParameterAttributeResolverInterface;
|
||||
use App\Framework\DI\Attributes\ParameterAttributeResolverRegistry;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\Logging\Attributes\LogChannel as LogChannelAttribute;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogChannel;
|
||||
|
||||
// Test class
|
||||
final class ServiceWithLogChannel
|
||||
{
|
||||
public function __construct(
|
||||
#[LogChannelAttribute(LogChannel::CACHE)]
|
||||
public Logger $logger
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mock Resolver für Tests
|
||||
final class MockAttributeResolver implements ParameterAttributeResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private bool $shouldSupport,
|
||||
private mixed $resolveValue
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(\ReflectionAttribute $attribute): bool
|
||||
{
|
||||
return $this->shouldSupport;
|
||||
}
|
||||
|
||||
public function resolve(
|
||||
\ReflectionParameter $param,
|
||||
ClassName $className,
|
||||
string $methodName
|
||||
): mixed {
|
||||
return $this->resolveValue;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = $this->createMock(Container::class);
|
||||
});
|
||||
|
||||
describe('ParameterAttributeResolverRegistry', function () {
|
||||
it('creates registry with variadic constructor', function () {
|
||||
$resolver1 = new MockAttributeResolver(true, 'value1');
|
||||
$resolver2 = new MockAttributeResolver(true, 'value2');
|
||||
|
||||
$registry = new ParameterAttributeResolverRegistry($resolver1, $resolver2);
|
||||
|
||||
expect($registry)->toBeInstanceOf(ParameterAttributeResolverRegistry::class);
|
||||
});
|
||||
|
||||
it('creates empty registry', function () {
|
||||
$registry = new ParameterAttributeResolverRegistry();
|
||||
|
||||
expect($registry)->toBeInstanceOf(ParameterAttributeResolverRegistry::class);
|
||||
});
|
||||
|
||||
it('uses first resolver that supports attribute', function () {
|
||||
$resolver1 = new MockAttributeResolver(false, 'value1');
|
||||
$resolver2 = new MockAttributeResolver(true, 'value2');
|
||||
$resolver3 = new MockAttributeResolver(true, 'value3');
|
||||
|
||||
$registry = new ParameterAttributeResolverRegistry($resolver1, $resolver2, $resolver3);
|
||||
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithLogChannel::class, '__construct'],
|
||||
0
|
||||
);
|
||||
$attribute = $reflectionParam->getAttributes()[0] ?? null;
|
||||
|
||||
if ($attribute) {
|
||||
$className = ClassName::create(ServiceWithLogChannel::class);
|
||||
$result = $registry->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
// Der zweite Resolver sollte verwendet werden (erste der supports = true)
|
||||
expect($result)->toBe('value2');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null when no resolver supports attribute', function () {
|
||||
$resolver1 = new MockAttributeResolver(false, 'value1');
|
||||
$resolver2 = new MockAttributeResolver(false, 'value2');
|
||||
|
||||
$registry = new ParameterAttributeResolverRegistry($resolver1, $resolver2);
|
||||
|
||||
$reflectionParam = new \ReflectionParameter(
|
||||
[ServiceWithLogChannel::class, '__construct'],
|
||||
0
|
||||
);
|
||||
$className = ClassName::create(ServiceWithLogChannel::class);
|
||||
$result = $registry->resolve($reflectionParam, $className, '__construct');
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('works with real resolvers', function () {
|
||||
$logChannelResolver = new LogChannelAttributeResolver($this->container);
|
||||
$envResolver = new EnvAttributeResolver($this->container);
|
||||
|
||||
$registry = new ParameterAttributeResolverRegistry($logChannelResolver, $envResolver);
|
||||
|
||||
expect($registry)->toBeInstanceOf(ParameterAttributeResolverRegistry::class);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,9 @@ use App\Framework\Logging\LogChannel;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\SupportsChannels;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Config\Env as EnvAttribute;
|
||||
use App\Framework\Config\EnvKey;
|
||||
use App\Framework\Config\Environment;
|
||||
|
||||
// Test classes for parameter resolution
|
||||
final class ServiceWithLogChannelAttribute
|
||||
@@ -88,6 +91,29 @@ final class ServiceWithMixedParams
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithNullableInterface
|
||||
{
|
||||
public function __construct(public ?Logger $optionalLogger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithNullableInterfaceResolved
|
||||
{
|
||||
public function __construct(public ?Logger $optionalLogger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceWithEnvAttribute
|
||||
{
|
||||
public function __construct(
|
||||
#[EnvAttribute(EnvKey::APP_NAME)]
|
||||
public string $appName
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = $this->createMock(Container::class);
|
||||
$this->reflectionProvider = new CachedReflectionProvider();
|
||||
@@ -200,6 +226,42 @@ describe('ParameterResolver', function () {
|
||||
expect($params[0])->toBeNull();
|
||||
});
|
||||
|
||||
it('resolves nullable interface from container when available', function () {
|
||||
$logger = $this->createMock(Logger::class);
|
||||
|
||||
$this->container->expects($this->once())
|
||||
->method('has')
|
||||
->with(Logger::class)
|
||||
->willReturn(true);
|
||||
|
||||
$this->container->expects($this->once())
|
||||
->method('get')
|
||||
->with(Logger::class)
|
||||
->willReturn($logger);
|
||||
|
||||
$className = ClassName::create(ServiceWithNullableInterfaceResolved::class);
|
||||
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
||||
|
||||
expect($params)->toHaveCount(1);
|
||||
expect($params[0])->toBe($logger);
|
||||
});
|
||||
|
||||
it('resolves nullable interface to null when container does not have it', function () {
|
||||
$this->container->expects($this->exactly(2))
|
||||
->method('has')
|
||||
->with(Logger::class)
|
||||
->willReturn(false);
|
||||
|
||||
$this->container->expects($this->never())
|
||||
->method('get');
|
||||
|
||||
$className = ClassName::create(ServiceWithNullableInterface::class);
|
||||
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
||||
|
||||
expect($params)->toHaveCount(1);
|
||||
expect($params[0])->toBeNull();
|
||||
});
|
||||
|
||||
it('throws exception when parameter cannot be resolved', function () {
|
||||
$this->container->method('has')->willReturn(false);
|
||||
|
||||
@@ -253,4 +315,24 @@ describe('ParameterResolver', function () {
|
||||
// Nullable
|
||||
expect($params[4])->toBeNull();
|
||||
});
|
||||
|
||||
it('resolves Env attribute from environment', function () {
|
||||
$environment = $this->createMock(Environment::class);
|
||||
|
||||
$environment->expects($this->once())
|
||||
->method('getString')
|
||||
->with(EnvKey::APP_NAME, '')
|
||||
->willReturn('Test App Name');
|
||||
|
||||
$this->container->expects($this->once())
|
||||
->method('get')
|
||||
->with(Environment::class)
|
||||
->willReturn($environment);
|
||||
|
||||
$className = ClassName::create(ServiceWithEnvAttribute::class);
|
||||
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
||||
|
||||
expect($params)->toHaveCount(1);
|
||||
expect($params[0])->toBe('Test App Name');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user