Skip to content

Commit 4d78d84

Browse files
committed
feat(logging): introduce SilentLogger for null-safe logging
Previously, components requiring a logger had to handle cases where one might not be provided, often leading to nullable properties (`?Logger`) and conditional checks. This increased complexity and the risk of fatal errors if a null logger was used. This commit introduces the `SilentLogger`, a new class that implements the Null Object Pattern for the Logger interface. - The `SilentLogger` can be used as a default or fallback logger. - It silently discards all log messages it receives, ensuring that calls to methods like `info()` or `error()` are always safe, even when no actual logging is configured. - This eliminates the need for `null` checks in client code (e.g., services, command executors), resulting in a cleaner and more robust design. A corresponding PHPUnit test (`SilentLoggerTest.php`) is included to ensure the class adheres to the Logger contract and functions correctly.
1 parent 175181c commit 4d78d84

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

src/SilentLogger.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KaririCode\Logging;
6+
7+
use KaririCode\Contract\Logging\Logger;
8+
use KaririCode\Contract\Logging\LogLevel;
9+
use KaririCode\Logging\Trait\LoggerTrait;
10+
11+
/**
12+
* Implements the Null Object Pattern for the Logger interface using a semantic name.
13+
*
14+
* This logger silently discards all log records it receives. It can be used
15+
* as a default or fallback logger to disable logging for a specific channel
16+
* or to prevent errors when a logger instance is not available, completely
17+
* eliminating the need for conditional checks (`if ($logger)`).
18+
*/
19+
final class SilentLogger implements Logger
20+
{
21+
use LoggerTrait;
22+
23+
public function __construct(private readonly string $name = 'silent')
24+
{
25+
}
26+
27+
/**
28+
* Silently discards any log message sent to it.
29+
*
30+
* This method is the core of the Null Object Pattern for this class.
31+
* It accepts all the parameters required by the Logger interface but
32+
* intentionally has no implementation.
33+
*/
34+
public function log(LogLevel $level, \Stringable|string $message, array $context = []): void
35+
{
36+
// This logger does nothing, by design.
37+
}
38+
39+
/**
40+
* Returns the name of this logger instance.
41+
*/
42+
public function getName(): string
43+
{
44+
return $this->name;
45+
}
46+
}

tests/Logger/SilentLoggerTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KaririCode\Logging\Tests\Logger;
6+
7+
use KaririCode\Contract\Logging\Logger;
8+
use KaririCode\Logging\LogLevel;
9+
use KaririCode\Logging\SilentLogger;
10+
use PHPUnit\Framework\Attributes\CoversClass;
11+
use PHPUnit\Framework\TestCase;
12+
13+
#[CoversClass(SilentLogger::class)]
14+
final class SilentLoggerTest extends TestCase
15+
{
16+
/**
17+
* Tests that the logger can be instantiated with its default name.
18+
*/
19+
public function testShouldBeInstantiableWithDefaultName(): void
20+
{
21+
// Arrange
22+
$logger = new SilentLogger();
23+
24+
// Assert
25+
$this->assertInstanceOf(Logger::class, $logger, 'SilentLogger must implement the Logger interface.');
26+
$this->assertEquals('silent', $logger->getName(), 'The default name for the logger should be "silent".');
27+
}
28+
29+
/**
30+
* Tests that the logger can be instantiated with a custom name.
31+
*/
32+
public function testShouldBeInstantiableWithCustomName(): void
33+
{
34+
// Arrange
35+
$logger = new SilentLogger('custom_channel');
36+
37+
// Assert
38+
$this->assertEquals('custom_channel', $logger->getName(), 'The logger should accept and return a custom name.');
39+
}
40+
41+
/**
42+
* Tests that calling the main log() method does not produce any errors or output.
43+
* This is the core behavior of the Null Object Pattern.
44+
*/
45+
public function testLogMethodShouldDoNothingAndNotThrowErrors(): void
46+
{
47+
// Arrange
48+
$logger = new SilentLogger();
49+
$this->expectNotToPerformAssertions(); // The assertion is that no error/exception occurs.
50+
51+
// Act
52+
$logger->log(LogLevel::INFO, 'This message should be discarded silently.');
53+
}
54+
55+
/**
56+
* Tests that convenience methods like info(), error(), etc., can be called safely.
57+
* These methods are provided by the LoggerTrait and rely on the empty log() method.
58+
*/
59+
public function testConvenienceMethodsShouldDoNothingAndNotThrowErrors(): void
60+
{
61+
// Arrange
62+
$logger = new SilentLogger();
63+
$this->expectNotToPerformAssertions();
64+
65+
// Act
66+
$logger->info('Info message to be discarded.');
67+
$logger->error('Error message to be discarded.', ['exception' => 'details']);
68+
$logger->warning('Warning message to be discarded.');
69+
}
70+
}

0 commit comments

Comments
 (0)