- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
257 lines
8.0 KiB
PHP
257 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Framework\DI;
|
|
|
|
use App\Framework\Core\ValueObjects\ClassName;
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\DI\ParameterResolver;
|
|
use App\Framework\Logging\Attributes\LogChannel as LogChannelAttribute;
|
|
use App\Framework\Logging\DefaultLogger;
|
|
use App\Framework\Logging\HasChannel;
|
|
use App\Framework\Logging\LogChannel;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Logging\SupportsChannels;
|
|
use App\Framework\Reflection\CachedReflectionProvider;
|
|
|
|
// Test classes for parameter resolution
|
|
final class ServiceWithLogChannelAttribute
|
|
{
|
|
public function __construct(
|
|
#[LogChannelAttribute(LogChannel::CACHE)]
|
|
public Logger $logger
|
|
) {
|
|
}
|
|
}
|
|
|
|
final class ServiceWithMultipleLogChannels
|
|
{
|
|
public function testMethod(
|
|
#[LogChannelAttribute(LogChannel::SECURITY)]
|
|
Logger $securityLogger,
|
|
#[LogChannelAttribute(LogChannel::DATABASE)]
|
|
Logger $dbLogger,
|
|
#[LogChannelAttribute(LogChannel::CACHE)]
|
|
Logger $cacheLogger
|
|
): void {
|
|
}
|
|
}
|
|
|
|
final class ServiceWithOverrideTest
|
|
{
|
|
public function __construct(
|
|
#[LogChannelAttribute(LogChannel::CACHE)]
|
|
public Logger $logger
|
|
) {
|
|
}
|
|
}
|
|
|
|
final class ServiceWithTypedDependency
|
|
{
|
|
public function __construct(public Logger $logger)
|
|
{
|
|
}
|
|
}
|
|
|
|
final class ServiceWithDefaultValue
|
|
{
|
|
public function __construct(public string $name = 'default-value')
|
|
{
|
|
}
|
|
}
|
|
|
|
final class ServiceWithNullable
|
|
{
|
|
public function __construct(public ?string $optionalParam)
|
|
{
|
|
}
|
|
}
|
|
|
|
final class ServiceWithRequiredParam
|
|
{
|
|
public function __construct(public string $requiredParam)
|
|
{
|
|
}
|
|
}
|
|
|
|
final class ServiceWithMixedParams
|
|
{
|
|
public function complexMethod(
|
|
#[LogChannelAttribute(LogChannel::FRAMEWORK)]
|
|
Logger $attributeLogger,
|
|
Logger $overriddenParam,
|
|
Logger $containerParam,
|
|
string $defaultParam = 'default',
|
|
?string $nullableParam = null
|
|
): void {
|
|
}
|
|
}
|
|
|
|
beforeEach(function () {
|
|
$this->container = $this->createMock(Container::class);
|
|
$this->reflectionProvider = new CachedReflectionProvider();
|
|
$this->resolver = new ParameterResolver($this->container, $this->reflectionProvider);
|
|
});
|
|
|
|
describe('ParameterResolver', function () {
|
|
it('resolves parameters with LogChannel attribute', function () {
|
|
// Mock main logger - use real DefaultLogger for simplicity
|
|
$mainLogger = new DefaultLogger();
|
|
|
|
$this->container->expects($this->once())
|
|
->method('get')
|
|
->with(SupportsChannels::class)
|
|
->willReturn($mainLogger);
|
|
|
|
$className = ClassName::create(ServiceWithLogChannelAttribute::class);
|
|
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
|
|
|
expect($params)->toHaveCount(1);
|
|
expect($params[0])->toBeInstanceOf(Logger::class);
|
|
expect($params[0])->toBeInstanceOf(HasChannel::class);
|
|
expect($params[0]->channel)->toBe(LogChannel::CACHE);
|
|
});
|
|
|
|
it('resolves different channel loggers via attribute', function () {
|
|
$mainLogger = new DefaultLogger();
|
|
|
|
$this->container->expects($this->exactly(3))
|
|
->method('get')
|
|
->with(SupportsChannels::class)
|
|
->willReturn($mainLogger);
|
|
|
|
$className = ClassName::create(ServiceWithMultipleLogChannels::class);
|
|
$params = $this->resolver->resolveMethodParameters($className, 'testMethod');
|
|
|
|
expect($params)->toHaveCount(3);
|
|
|
|
// Verify each logger has correct channel
|
|
expect($params[0])->toBeInstanceOf(Logger::class);
|
|
expect($params[0])->toBeInstanceOf(HasChannel::class);
|
|
expect($params[0]->channel)->toBe(LogChannel::SECURITY);
|
|
|
|
expect($params[1])->toBeInstanceOf(Logger::class);
|
|
expect($params[1])->toBeInstanceOf(HasChannel::class);
|
|
expect($params[1]->channel)->toBe(LogChannel::DATABASE);
|
|
|
|
expect($params[2])->toBeInstanceOf(Logger::class);
|
|
expect($params[2])->toBeInstanceOf(HasChannel::class);
|
|
expect($params[2]->channel)->toBe(LogChannel::CACHE);
|
|
});
|
|
|
|
it('resolves override values before attributes', function () {
|
|
// Container should NOT be called because override takes priority
|
|
$this->container->expects($this->never())
|
|
->method('get');
|
|
|
|
$overrideLogger = $this->createMock(Logger::class);
|
|
|
|
$className = ClassName::create(ServiceWithOverrideTest::class);
|
|
$params = $this->resolver->resolveMethodParameters(
|
|
$className,
|
|
'__construct',
|
|
['logger' => $overrideLogger]
|
|
);
|
|
|
|
expect($params)->toHaveCount(1);
|
|
expect($params[0])->toBe($overrideLogger);
|
|
});
|
|
|
|
it('resolves type-based injection when no attribute present', 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(ServiceWithTypedDependency::class);
|
|
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
|
|
|
expect($params)->toHaveCount(1);
|
|
expect($params[0])->toBe($logger);
|
|
});
|
|
|
|
it('resolves default values when no injection possible', function () {
|
|
$this->container->expects($this->never())
|
|
->method('get');
|
|
|
|
$className = ClassName::create(ServiceWithDefaultValue::class);
|
|
$params = $this->resolver->resolveMethodParameters($className, '__construct');
|
|
|
|
expect($params)->toHaveCount(1);
|
|
expect($params[0])->toBe('default-value');
|
|
});
|
|
|
|
it('resolves nullable parameters to null', function () {
|
|
$this->container->expects($this->never())
|
|
->method('get');
|
|
|
|
$className = ClassName::create(ServiceWithNullable::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);
|
|
|
|
$className = ClassName::create(ServiceWithRequiredParam::class);
|
|
|
|
expect(fn () => $this->resolver->resolveMethodParameters($className, '__construct'))
|
|
->toThrow(\InvalidArgumentException::class, 'Cannot resolve parameter');
|
|
});
|
|
|
|
it('follows correct resolution priority order', function () {
|
|
$mainLogger = new DefaultLogger();
|
|
$overrideLogger = $this->createMock(Logger::class);
|
|
$containerLogger = $this->createMock(Logger::class);
|
|
|
|
$this->container->method('get')
|
|
->willReturnCallback(function ($type) use ($mainLogger, $containerLogger) {
|
|
return match ($type) {
|
|
SupportsChannels::class => $mainLogger,
|
|
Logger::class => $containerLogger,
|
|
default => throw new \RuntimeException("Unexpected type: {$type}")
|
|
};
|
|
});
|
|
|
|
$this->container->method('has')
|
|
->with(Logger::class)
|
|
->willReturn(true);
|
|
|
|
$className = ClassName::create(ServiceWithMixedParams::class);
|
|
$params = $this->resolver->resolveMethodParameters(
|
|
$className,
|
|
'complexMethod',
|
|
['overriddenParam' => $overrideLogger]
|
|
);
|
|
|
|
expect($params)->toHaveCount(5);
|
|
|
|
// Attribute injection
|
|
expect($params[0])->toBeInstanceOf(Logger::class);
|
|
expect($params[0])->toBeInstanceOf(HasChannel::class);
|
|
expect($params[0]->channel)->toBe(LogChannel::FRAMEWORK);
|
|
|
|
// Override
|
|
expect($params[1])->toBe($overrideLogger);
|
|
|
|
// Container
|
|
expect($params[2])->toBe($containerLogger);
|
|
|
|
// Default value
|
|
expect($params[3])->toBe('default');
|
|
|
|
// Nullable
|
|
expect($params[4])->toBeNull();
|
|
});
|
|
});
|