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;
|
namespace App\Framework\DI;
|
||||||
|
|
||||||
use App\Framework\Core\ValueObjects\ClassName;
|
use App\Framework\Core\ValueObjects\ClassName;
|
||||||
use App\Framework\Logging\Attributes\LogChannel;
|
use App\Framework\DI\Attributes\EnvAttributeResolver;
|
||||||
use App\Framework\Logging\SupportsChannels;
|
use App\Framework\DI\Attributes\LogChannelAttributeResolver;
|
||||||
|
use App\Framework\DI\Attributes\ParameterAttributeResolverRegistry;
|
||||||
use App\Framework\Reflection\ReflectionProvider;
|
use App\Framework\Reflection\ReflectionProvider;
|
||||||
use App\Framework\Reflection\WrappedReflectionParameter;
|
use App\Framework\Reflection\WrappedReflectionParameter;
|
||||||
|
|
||||||
@@ -19,10 +20,18 @@ use App\Framework\Reflection\WrappedReflectionParameter;
|
|||||||
*/
|
*/
|
||||||
final readonly class ParameterResolver
|
final readonly class ParameterResolver
|
||||||
{
|
{
|
||||||
|
private readonly ParameterAttributeResolverRegistry $attributeResolverRegistry;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Container $container,
|
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.)
|
* 2. Attribute-basierte Injection (#[LogChannel], etc.)
|
||||||
* 3. Type-basierte Injection vom Container
|
* 3. Type-basierte Injection vom Container
|
||||||
* 4. Default-Werte
|
* 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
|
* @param array<string, mixed> $overrides Named parameter overrides
|
||||||
* @return array<int, mixed> Resolved parameter values
|
* @return array<int, mixed> Resolved parameter values
|
||||||
@@ -86,7 +96,17 @@ final readonly class ParameterResolver
|
|||||||
return $param->getDefaultValue();
|
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()) {
|
if ($param->allowsNull()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -100,48 +120,23 @@ final readonly class ParameterResolver
|
|||||||
/**
|
/**
|
||||||
* Versucht Parameter über Attributes aufzulösen
|
* Versucht Parameter über Attributes aufzulösen
|
||||||
*
|
*
|
||||||
* Nutzt direkt die native Reflection API für maximale Einfachheit
|
* Verwendet die ParameterAttributeResolverRegistry, um verschiedene
|
||||||
*
|
* Attribute zu behandeln. Unterstützte Attributes werden über die
|
||||||
* Unterstützte Attributes:
|
* Registry aufgelöst.
|
||||||
* - #[LogChannel] - Injiziert Channel-spezifischen Logger
|
|
||||||
*/
|
*/
|
||||||
private function resolveFromAttributes(
|
private function resolveFromAttributes(
|
||||||
WrappedReflectionParameter $param,
|
WrappedReflectionParameter $param,
|
||||||
ClassName $className,
|
ClassName $className,
|
||||||
string $methodName
|
string $methodName
|
||||||
): mixed {
|
): mixed {
|
||||||
// Direkt native Reflection API nutzen
|
// Direkt native Reflection API nutzen für Attribute-Zugriff
|
||||||
$nativeParam = new \ReflectionParameter(
|
$nativeParam = new \ReflectionParameter(
|
||||||
[$className->getFullyQualified(), $methodName],
|
[$className->getFullyQualified(), $methodName],
|
||||||
$param->getPosition()
|
$param->getPosition()
|
||||||
);
|
);
|
||||||
|
|
||||||
// #[LogChannel] Attribut
|
// Verwende Registry, um Attribute aufzulösen
|
||||||
$logChannelAttrs = $nativeParam->getAttributes(LogChannel::class);
|
return $this->attributeResolverRegistry->resolve($nativeParam, $className, $methodName);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,4 +156,21 @@ final readonly class ParameterResolver
|
|||||||
|
|
||||||
return null;
|
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\Logger;
|
||||||
use App\Framework\Logging\SupportsChannels;
|
use App\Framework\Logging\SupportsChannels;
|
||||||
use App\Framework\Reflection\CachedReflectionProvider;
|
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
|
// Test classes for parameter resolution
|
||||||
final class ServiceWithLogChannelAttribute
|
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 () {
|
beforeEach(function () {
|
||||||
$this->container = $this->createMock(Container::class);
|
$this->container = $this->createMock(Container::class);
|
||||||
$this->reflectionProvider = new CachedReflectionProvider();
|
$this->reflectionProvider = new CachedReflectionProvider();
|
||||||
@@ -200,6 +226,42 @@ describe('ParameterResolver', function () {
|
|||||||
expect($params[0])->toBeNull();
|
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 () {
|
it('throws exception when parameter cannot be resolved', function () {
|
||||||
$this->container->method('has')->willReturn(false);
|
$this->container->method('has')->willReturn(false);
|
||||||
|
|
||||||
@@ -253,4 +315,24 @@ describe('ParameterResolver', function () {
|
|||||||
// Nullable
|
// Nullable
|
||||||
expect($params[4])->toBeNull();
|
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