DEV Community

Abu Jobaer
Abu Jobaer

Posted on

Create your own Logger using Open-closed principle

We will create a logger application that adheres to the Open-Closed Principle (OCP), one of the SOLID principles. I have provided a detailed explanation of the OCP here if you’re unfamiliar with it.

The main purpose of the Open-Closed Principle (OCP) is to allow the extension of functionality without altering existing code — whether it was written by you or provided by a library. This application follows OCP by enabling the addition of new logging drivers without modifying existing code.

What We Need

Let’s take a look at what is needed to create our own logger application. In this implementation, we’ll require a logger interface, several concrete logger implementations, and a logger manager.

Logger Interface: Defines the contract for all logger implementations — LoggerInterface.

Concrete Loggers: Implements the logger interface for different drivers — FileLogger, DatabaseLogger, EmailLogger, etc.

Logger Manager: Acts as a central hub to manage and use various loggers.

Client Code

Let’s begin with the client code. First, we create instances of specific logger implementations: FileLogger, DatabaseLogger, and EmailLogger. Next, these loggers are registered with the LoggerManager by associating a driver type with each logger instance. Finally, we use the log() method of the LoggerManager to log messages by specifying the driver type and the message. This design allows flexible logging to any registered drivers, making the system extensible and adaptable.

// Create loggers
$fileLogger = new FileLogger(__DIR__ .  '/logs/app.log');
$dbLogger = new DatabaseLogger(new PDO('sqlite:logs.db'), 'logs');
$emailLogger = new EmailLogger('admin@example.com');

// Logger Manager
$loggerManager = new LoggerManager();
$loggerManager->addLogger('file', $fileLogger);
$loggerManager->addLogger('database', $dbLogger);
$loggerManager->addLogger('email', $emailLogger);

// Log messages
$loggerManager->log('file', 'This is a log message for the file logger.');
$loggerManager->log('database', 'This is a log message for the database logger.');
$loggerManager->log('email', 'This is a log message for the email logger.');
Enter fullscreen mode Exit fullscreen mode

Logger Interface
To follow the principle of ‘Program to an interface, not an implementation’ in building the logger application, we first need to define the interface. Let’s create the LoggerInterface:

interface LoggerInterface
{
    public function log(string $message): void;
}
Enter fullscreen mode Exit fullscreen mode

Here the LoggerInterface defines an abstract log() method which serves as the foundation of the logger application, ensuring messages are logged to the designated driver.

Logger Manager
The logger manager acts as a centralized way to manage and use loggers. It decouples the client code from the concrete implementations.

class LoggerManager
{
    private array $loggers = [];

    public function addLogger(string $name, LoggerInterface $logger): void
    {
        $this->loggers[$name] = $logger;
    }

    public function log(string $name, string $message): void
    {
        if (!isset($this->loggers[$name])) {
            throw new InvalidArgumentException("Logger '{$name}' not found.");
        }
        $this->loggers[$name]->log($message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that the addLogger() method accepts a LoggerInterface as its second parameter. This allows you to pass any object that implements the LoggerInterface as an argument. Additionally, the log() method in LoggerManager invokes the log() method defined by the LoggerInterface, ensuring consistency across all logger implementations.

Concrete Loggers
Now we need some concrete loggers. Let’s create them:

// File Logger
class FileLogger implements LoggerInterface
{
    private string $filePath;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }

    public function log(string $message): void
    {
        file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
    }
}

// Database Logger
class DatabaseLogger implements LoggerInterface
{
    private PDO $connection;

    private string $table;

    public function __construct(PDO $connection, string $table)
    {
        $this->connection = $connection;
        $this->table = $table;
    }

    public function log(string $message): void
    {
        $stmt = $this->connection->prepare("INSERT INTO {$this->table} (message, created_at) VALUES (:message, NOW())");
        $stmt->execute(['message' => $message]);
    }
}

// Email Logger
class EmailLogger implements LoggerInterface
{
    private string $recipient;

    public function __construct(string $recipient)
    {
        $this->recipient = $recipient;
    }

    public function log(string $message): void
    {
        mail($this->recipient, 'Log Message', $message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep in mind that if you intend to use this application in production, the concrete logger classes will need significant improvement and redesign to align with best practices.

Is It OCP Compliant?

Yes, if your application doesn’t require advanced features such as logger prioritization — like using the DatabaseLogger first and falling back to the FileLogger — or dynamic runtime selection of loggers based on context, such as user roles or the environment.

The application is open for extension because new loggers can be added by implementing the LoggerInterface. It is closed for modification because the existing code does not need to change when adding new loggers.

Extending the Application

Now adding a new driver is easy. To add a new logger, such as a SlackLogger, simply create a class that implements the LoggerInterface and register it with the LoggerManager for seamless integration.

That’s it.

What’s next? Consider enhancing the EmailLogger class to support multiple email recipients and utilize a queue for sending emails instead of processing them synchronously. Additionally, you could improve the DatabaseLogger class by implementing the Singleton design pattern for database connections, avoiding the need to create a new connection object each time the log() method is called. These are just a few possibilities for further improvement.

Since you’ve made it this far, hopefully you enjoyed the reading! Please share the article.

Follow me on:
LinkedIn, Medium, and Github

Top comments (0)