backend, Locastic Apr 19

Building a multi-file static logger with PHP

4 min read –
multi static file logger featured photo for danijela blog php

You may have heard that static functions are difficult to change, impossible to test, limiting, against the principles of object-oriented programming. They ruin everything good in life, and you can basically describe them as the root of all evil.

While some of these accusations might be true, I think it is possible to find valid usages of all-static classes and still keep a good structure and maintainability of an OOP PHP application, that is, if you keep them on simple enough level.

Static class, meet logger. Logger, meet static class.

One of the problems that static functions solve well is writing to log files. There are a lot of non-static logger solutions out there, like monolog or Psr Log, but I wanted mine to be static so I can use it easily without any additional dependencies or even worse, global instances. This logger needed to log every entry to the database during the import of products, categories, prices and other webshop-related data from an external ERP solution.

The “catch” was that the log file should only exist during the execution of the import command and you had to delete it if no error happened. So, to begin, I wrote the following Logger class:

class Logger
{
   protected static $fileName = 'import.log';
   public static function writeLog($message)
   {
       if (false === file_exists(realpath(self::$fileName))) {
             return;
       }
       $file = self::openFile();
       fwrite($file, $message."\n");
   }
   public static function createFile()
   {
       return fopen(self::$fileName, 'wb');
   }
   public static function closeFile()
   {
       $file = fopen(self::$fileName, 'rb');
       fclose($file);
   }
   public static function removeFile()
   {
       unlink(self::$fileName);
   }
   private static function openFile()
   {
       return fopen(self::$fileName, 'ab');
   }
}

The first function you need to run in order to log something is createFile function which creates the log files. writeLog function writes the actual log message to the file if the file exists. Other functions are pretty much self-explanatory. If you wanted to write a message to log file, you would do it like this:

Logger::createFile();
Logger::writeLog("You're a wizard, Harry.");

If you work as a software developer, you are probably aware that feature requests change as fast as lightning. This one was no different. The new functionality that I needed to implement is that every entity has its own log file. The only difference between those files is their name. This is when inheritance comes in handy. But you can’t extend static classes, right?

Wrong!

This is where late static binding (php.net) comes to the rescue. Late static binding is a neat little feature in PHP that I discovered only recently. You would probably use self:: to access the class static property or function, but self:: is very limited.

The problem in this case with self:: is that the variable we are trying to access is always the one inside the base class, even if you override it in the extending class. This means no inheritance is possible in a static context. To solve this issue, PHP introduced static::. static is a keyword used to indicate late static binding.

This means that static keyword holds a reference to the class called in runtime. By using static:: instead of self:: to access an overridden variable in the static context, you will get the value of the variable from the extending class – and voila! You have an inheritance in static classes. So let’s apply this new, awesome knowledge to our logger:

class Logger
{
   protected static $fileName = 'import.log';
   public static function writeLog($message)
   {
       if (false === file_exists(realpath(static::$fileName))) {
           return;
       }
       $file = static::openFile();
       fwrite($file, $message."\n");
   }
   public static function createFile()
   {
       return fopen(static::$fileName, 'wb');
   }
   public static function closeFile()
   {
       $file = fopen(static::$fileName, 'rb');
       fclose($file);
   }
   public static function removeFile()
   {
       unlink(static::$fileName);
   }
   private static function openFile()
   {
       return fopen(static::$fileName, 'ab');
   }
}
_________________________________________________________________
class ProductImportLogger extends Logger
{
   protected static $fileName = "import-products.log";
}
_________________________________________________________________
class PricesImportLogger extends Logger
{
   protected static $fileName = "import-prices.log";
}
_________________________________________________________________
class StockImportLogger extends Logger
{
   protected static $fileName = "import-stock.log";
}

Now we have a ProductImportLogger, PricesImportLogger and StockImportLogger classes extending Logger class. The only thing we need to do in the extending classes is to override the $fileNamevariable, since it’s the only difference between the log files. The one thing changed to the previous code in Logger class is access to $fileName variable with static:: instead of self::. In this way, if you wanted to write different messages to each log file, you would call it like this:

ProductImportLogger::createFile();
ProductImportLogger::writeLog("You're a wizard, Harry.");

PricesImportLogger::createFile();
PricesImportLogger::writeLog("They may take our lives, but they'll never take our freedom!");

StockImportLogger::createFile();
StockImportLogger::writeLog("Magic Mirror on the wall, who is the fairest one of all?");

This already looks pretty nice, but, as I am lazy, I wanted it to be even simpler to use. I wanted the logger class to have one writeLogfunction that would “decide” on it’s own to which log file to write.

Pushing inheritance to the max

To remind you, the Logger class should only be able to write to a log file only if the file already exists. This means I can call the writeLog function on whichever class extending the Logger class and it would write to the right one if I called the createFile function at the right time. So, I created a writeLogMessage function:

class Logger
{
    //…
    
    public static function writeLogMessage($message)
    {
        StockImportLogger::writeLog($message);
        PricesImportLogger::writeLog($message);
        ProductImportLogger::writeLog($message);
        return true;
    }
    //…
}

After this, I could write to logs like this:

ProductImportLogger::createFile();
ProductImportLogger::writeLogMessage("You're a wizard, Harry.");

PricesImportLogger::createFile();
PricesImportLogger::writeLogMessage("They may take our lives, but they'll never take our freedom!");

StockImportLogger::createFile();
StockImportLogger::writeLogMessage("Magic Mirror on the wall, who is the fairest one of all?");

Which is pretty much the same as before. But, let’s look at this statement:

ProductImportLogger::createFile();

ProductImportLogger is nothing more than a string containing a path to ProductImportLogger class. Since I have a command which handles the import of data, and depending on which entity it is operating on at that moment – it needs to write info to the belonging log file. So in the moment it knows which kind of entity it is handling, the new variable $logger is set. The value of $logger variable is the path to the correct logger class (eg. PricesImportLogger::class).

Now we can call the logger like this:

$logger = ProductImportLogger::class;//this is handled through
                                     // dependency injection IRL 
                                     //and it already has a value   
                                     //when class is loaded
$logger::createFile();
$logger::writeLog("You're a wizard, Harry.");

With all this set up, we have a static logger system which can write to a specified logger file by calling only two static functions. It is easy to maintain and alter since basically, all the code is in one class only. The testing part of this logger is still a problem, but this feature is actually easily testable by hand, so this wasn’t that much of a problem for me.

Admittedly, this example is very project-specific, I do hope that you found some useful applications or ideas for applications of static classes and inheritance that you could implement for your needs.