Where are loggers used?
In general there are two major use-cases for use of loggers within your code:
invasive logging:
For the most part people use this approach because it is the easiest to understand.
In reality you should only use invasive logging if logging is part of the domain logic itself. For example - in classes that deal with payments or management of sensitive information.
Non-invasive logging:
With this method instead of altering the class that you wish to log, you wrap an existing instance in a container that lets you track every exchange between instance and rest of application.
You also gain the ability to enable such logging temporarily, while debugging some specific problem outside of the development environment or when you are conducting some research of user behaviour. Since the class of the logged instance is never altered, the risk of disrupting the project's behaviour is a lot lower when compared to invasive logging.
Implementing an invasive logger
To do this you have two main approaches available. You can either inject an instance that implements the Logger
interface, or provide the class with a factory that in turn will initialize the logging system only when necessary.
Note:
Since it seems that direct injection is not some hidden mystery for you, I will leave that part out... only I would urge you to avoid using constants outside of a file where they have been defined.
Now .. the implementation with factory and lazy loading.
You start by defining the API that you will use (in perfect world you start with unit-tests).
class Foobar
{
private $loggerFactory;
public function __construct(Creator $loggerFactory, ....)
{
$this->loggerFactory = $loggerFactory;
....
}
....
public function someLoggedMethod()
{
$logger = $this->loggerFactory->provide('simple');
$logger->log( ... logged data .. );
....
}
....
}
This factory will have two additional benefits:
- it can ensure that only one instance is created without a need for global state
- provide a seam for use when writing unit-tests
Note:
Actually, when written this way the class Foobar only depends on an instance that implements the Creator interface. Usually you will inject either a builder (if you need to type of instance, probably with some setting) or a factory (if you want to create different instance with same interface).
Next step would be implementation of the factory:
class LazyLoggerFactory implements Creator
{
private $loggers = [];
private $providers = [];
public function addProvider($name, callable $provider)
{
$this->providers[$name] = $provider;
return $this;
}
public function provide($name)
{
if (array_key_exists($name, $this->loggers) === false)
{
$this->loggers[$name] = call_user_func($this->providers[$name]);
}
return $this->loggers[$name];
}
}
When you call $factory->provide('thing');
, the factory looks up if the instance has already been created. If the search fails it creates a new instance.
Note: I am actually not entirely sure that this can be called "factory" since the instantiation is really encapsulated in the anonymous functions.
And the last step is actually wiring it all up with providers:
$config = include '/path/to/config/loggers.php';
$loggerFactory = new LazyLoggerFactory;
$loggerFactory->addProvider('simple', function() use ($config){
$instance = new SimpleFileLogger($config['log_file']);
return $instance;
});
/*
$loggerFactory->addProvider('fake', function(){
$instance = new NullLogger;
return $instance;
});
*/
$test = new Foobar( $loggerFactory );
Of course to fully understand this approach you will have to know how closures work in PHP, but you will have to learn them anyway.
Implementing non-invasive logging
The core idea of this approach is that instead of injecting the logger, you put an existing instance in a container which acts as membrane between said instance and application. This membrane can then perform different tasks, one of those is logging.
class LogBrane
{
protected $target = null;
protected $logger = null;
public function __construct( $target, Logger $logger )
{
$this->target = $target;
$this->logger = $logger;
}
public function __call( $method, $arguments )
{
if ( method_exists( $this->target, $method ) === false )
{
// sometime you will want to log call of nonexistent method
}
try
{
$response = call_user_func_array( [$this->target, $method],
$arguments );
// write log, if you want
$this->logger->log(....);
}
catch (Exception $e)
{
// write log about exception
$this->logger->log(....);
// and re-throw to not disrupt the behavior
throw $e;
}
}
}
This class can also be used together with the above described lazy factory.
To use this structure, you simply do the following:
$instance = new Foobar;
$instance = new LogBrane( $instance, $logger );
$instance->someMethod();
At this point the container which wraps the instance becomes a fully functional replacement of the original. The rest of your application can handle it as if it is a simple object (pass around, call methods upon). And the wrapped instance itself is not aware that it is being logged.
And if at some point you decide to remove the logging then it can be done without rewriting the rest of your application.