logCalls = []; $this->testLogger = new class($this->logCalls) implements Logger, SupportsChannels { public function __construct(private array &$logCalls) { } public function debug(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::DEBUG, $message, $context); } public function info(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::INFO, $message, $context); } public function notice(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::NOTICE, $message, $context); } public function warning(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::WARNING, $message, $context); } public function error(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::ERROR, $message, $context); } public function critical(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::CRITICAL, $message, $context); } public function alert(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::ALERT, $message, $context); } public function emergency(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::EMERGENCY, $message, $context); } public function log(LogLevel $level, string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, $level, $message, $context); } public function logToChannel( LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null ): void { $this->logCalls[] = [ 'channel' => $channel, 'level' => $level, 'message' => $message, 'context' => $context ]; } public function channel(LogChannel|string $channel): Logger&HasChannel { $logChannel = $channel instanceof LogChannel ? $channel : LogChannel::from($channel); return new DefaultChannelLogger($this, $logChannel); } }; $this->channelLogger = new DefaultChannelLogger($this->testLogger, LogChannel::SECURITY); }); describe('interface implementation', function () { it('implements Logger and HasChannel interfaces', function () { expect($this->channelLogger)->toBeInstanceOf(Logger::class); expect($this->channelLogger)->toBeInstanceOf(HasChannel::class); }); it('has correct channel property', function () { expect($this->channelLogger->channel)->toBe(LogChannel::SECURITY); }); }); describe('log level delegation', function () { it('delegates debug to parent logger', function () { $this->channelLogger->debug('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['channel'])->toBe(LogChannel::SECURITY); expect($this->logCalls[0]['level'])->toBe(LogLevel::DEBUG); expect($this->logCalls[0]['message'])->toBe('Test message'); }); it('delegates info to parent logger', function () { $this->channelLogger->info('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::INFO); }); it('delegates warning to parent logger', function () { $this->channelLogger->warning('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::WARNING); }); it('delegates error to parent logger', function () { $this->channelLogger->error('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::ERROR); }); it('delegates critical to parent logger', function () { $this->channelLogger->critical('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::CRITICAL); }); it('delegates alert to parent logger', function () { $this->channelLogger->alert('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::ALERT); }); it('delegates emergency to parent logger', function () { $this->channelLogger->emergency('Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::EMERGENCY); }); it('delegates generic log to parent logger', function () { $this->channelLogger->log(LogLevel::WARNING, 'Test message', LogContext::withData(['key' => 'value'])); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['level'])->toBe(LogLevel::WARNING); expect($this->logCalls[0]['message'])->toBe('Test message'); }); }); describe('context handling', function () { it('supports log context objects', function () { $logContext = LogContext::withData(['user_id' => 123]); $this->channelLogger->info('Test message', $logContext); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['context'])->toBeInstanceOf(LogContext::class); }); it('supports empty context', function () { $this->channelLogger->info('Test message'); expect(count($this->logCalls))->toBe(1); expect($this->logCalls[0]['message'])->toBe('Test message'); }); }); describe('channel independence', function () { it('different channels work independently', function () { $logCalls = []; $testLogger = new class($logCalls) implements Logger, SupportsChannels { public function __construct(private array &$logCalls) { } public function debug(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::DEBUG, $message, $context); } public function info(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::INFO, $message, $context); } public function notice(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::NOTICE, $message, $context); } public function warning(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, LogLevel::WARNING, $message, $context); } public function error(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::ERROR, $message, $context); } public function critical(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::CRITICAL, $message, $context); } public function alert(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::ALERT, $message, $context); } public function emergency(string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::ERROR, LogLevel::EMERGENCY, $message, $context); } public function log(LogLevel $level, string $message, ?LogContext $context = null): void { $this->logToChannel(LogChannel::FRAMEWORK, $level, $message, $context); } public function logToChannel( LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null ): void { $this->logCalls[] = ['channel' => $channel]; } public function channel(LogChannel|string $channel): Logger&HasChannel { $logChannel = $channel instanceof LogChannel ? $channel : LogChannel::from($channel); return new DefaultChannelLogger($this, $logChannel); } }; $cacheLogger = new DefaultChannelLogger($testLogger, LogChannel::CACHE); $dbLogger = new DefaultChannelLogger($testLogger, LogChannel::DATABASE); $securityLogger = new DefaultChannelLogger($testLogger, LogChannel::SECURITY); expect($cacheLogger->channel)->toBe(LogChannel::CACHE); expect($dbLogger->channel)->toBe(LogChannel::DATABASE); expect($securityLogger->channel)->toBe(LogChannel::SECURITY); }); }); });