testDir = sys_get_temp_dir() . '/logger_test_' . uniqid(); mkdir($this->testDir, 0777, true); $this->testLogFile = $this->testDir . '/test.log'; }); afterEach(function () { // Clean up test files if (file_exists($this->testLogFile)) { unlink($this->testLogFile); } if (is_dir($this->testDir)) { rmdir($this->testDir); } }); describe('constructor', function () { it('creates log file directory if it does not exist', function () { $logFile = $this->testDir . '/nested/dir/test.log'; $handler = new FileHandler($logFile); expect(is_dir(dirname($logFile)))->toBeTrue(); }); it('accepts LogLevel enum as minLevel', function () { $handler = new FileHandler($this->testLogFile, LogLevel::ERROR); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeFalse(); }); it('accepts int as minLevel', function () { $handler = new FileHandler($this->testLogFile, 400); // ERROR level $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeFalse(); }); it('defaults to DEBUG level when no minLevel specified', function () { $handler = new FileHandler($this->testLogFile); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeTrue(); }); }); describe('isHandling()', function () { it('returns true when record level is above minLevel', function () { $handler = new FileHandler($this->testLogFile, LogLevel::WARNING); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeTrue(); }); it('returns true when record level equals minLevel', function () { $handler = new FileHandler($this->testLogFile, LogLevel::WARNING); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeTrue(); }); it('returns false when record level is below minLevel', function () { $handler = new FileHandler($this->testLogFile, LogLevel::ERROR); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeFalse(); }); }); describe('handle()', function () { it('writes log entry to file', function () { $handler = new FileHandler($this->testLogFile); $record = new LogRecord( message: 'Test log message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); expect(file_exists($this->testLogFile))->toBeTrue(); $content = file_get_contents($this->testLogFile); expect($content)->toContain('Test log message'); }); it('includes level name in output', function () { $handler = new FileHandler($this->testLogFile); $record = new LogRecord( message: 'Test error', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('ERROR'); }); it('includes timestamp in output', function () { $handler = new FileHandler($this->testLogFile); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); $content = file_get_contents($this->testLogFile); // Check that a timestamp pattern exists (e.g., 2024-10-20 or similar) expect($content)->toMatch('/\d{4}-\d{2}-\d{2}/'); }); it('includes channel in output', function () { $handler = new FileHandler($this->testLogFile); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'security' ); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('[security]'); }); it('includes request_id when present in extras', function () { $handler = new FileHandler($this->testLogFile); $record = (new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ))->addExtra('request_id', 'req-123'); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('[req-123]'); }); it('appends to existing file', function () { $handler = new FileHandler($this->testLogFile); $record1 = new LogRecord( message: 'First message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $record2 = new LogRecord( message: 'Second message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record1); $handler->handle($record2); $content = file_get_contents($this->testLogFile); expect($content)->toContain('First message'); expect($content)->toContain('Second message'); // Check that we have two lines $lines = explode(PHP_EOL, trim($content)); expect(count($lines))->toBe(2); }); it('uses custom output format when specified', function () { $handler = new FileHandler( $this->testLogFile, outputFormat: '{level_name}: {message}' ); $record = new LogRecord( message: 'Custom format test', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('ERROR: Custom format test'); }); }); describe('setMinLevel()', function () { it('updates minimum level with LogLevel', function () { $handler = new FileHandler($this->testLogFile, LogLevel::DEBUG); $handler->setMinLevel(LogLevel::ERROR); $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeFalse(); }); it('updates minimum level with int', function () { $handler = new FileHandler($this->testLogFile, LogLevel::DEBUG); $handler->setMinLevel(400); // ERROR $record = new LogRecord( message: 'test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeFalse(); }); it('returns self for fluent API', function () { $handler = new FileHandler($this->testLogFile); $result = $handler->setMinLevel(LogLevel::ERROR); expect($result)->toBe($handler); }); }); describe('setOutputFormat()', function () { it('updates output format', function () { $handler = new FileHandler($this->testLogFile); $handler->setOutputFormat('{level_name} - {message}'); $record = new LogRecord( message: 'New format', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('INFO - New format'); }); it('returns self for fluent API', function () { $handler = new FileHandler($this->testLogFile); $result = $handler->setOutputFormat('{message}'); expect($result)->toBe($handler); }); }); describe('setLogFile()', function () { it('updates log file path', function () { $handler = new FileHandler($this->testLogFile); $newLogFile = $this->testDir . '/new-test.log'; $handler->setLogFile($newLogFile); $record = new LogRecord( message: 'New file test', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'test' ); $handler->handle($record); expect(file_exists($newLogFile))->toBeTrue(); $content = file_get_contents($newLogFile); expect($content)->toContain('New file test'); // Cleanup if (file_exists($newLogFile)) { unlink($newLogFile); } }); it('creates directory for new log file', function () { $handler = new FileHandler($this->testLogFile); $newLogFile = $this->testDir . '/nested/new/test.log'; $handler->setLogFile($newLogFile); expect(is_dir(dirname($newLogFile)))->toBeTrue(); // Cleanup if (file_exists($newLogFile)) { unlink($newLogFile); } // Clean directories from deepest to shallowest if (is_dir($this->testDir . '/nested/new')) { rmdir($this->testDir . '/nested/new'); } if (is_dir($this->testDir . '/nested')) { rmdir($this->testDir . '/nested'); } }); it('returns self for fluent API', function () { $handler = new FileHandler($this->testLogFile); $result = $handler->setLogFile($this->testDir . '/other.log'); expect($result)->toBe($handler); }); }); describe('fluent interface', function () { it('supports chaining methods', function () { $handler = new FileHandler($this->testLogFile); $handler ->setMinLevel(LogLevel::WARNING) ->setOutputFormat('{level_name}: {message}') ->setLogFile($this->testLogFile); $record = new LogRecord( message: 'Chained config', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: new DateTimeImmutable(), channel: 'test' ); expect($handler->isHandling($record))->toBeTrue(); $handler->handle($record); $content = file_get_contents($this->testLogFile); expect($content)->toContain('ERROR: Chained config'); }); }); });