By using a singleton here you would hide the dependency of the Logger. You don't need a global point of access here, and since you're already trying to adhere to DI, you probably don't want to clutter up your code and make it untestable.
Indeed there are cleaner ways to implement that. Let's go through it.
set_error_handler accepts objects
You don't need to pass a closure or a function name to the set_error_handler
function. Here's what the docs state:
A callback with the following signature. NULL may be passed instead, to reset this handler to its default state. Instead of a function name, an array containing an object reference and a method name can also be supplied.
Knowing this, you can use a dedicated object for handling errors. The handler method on the object will be called like this in set_error_handler
set_error_handler([$errorHandler, 'handle']);
where $errorHandler
is the object and handle
the method to be called.
The Error Handler
The ErrorHandler
class will be responsible for your error handling. The benefits we gain by using a class is that we can make use of DI easily.
<?php
interface ErrorHandler {
public function handle( $errno, $errstr , $errfile = null , $errline = null , $errcontext = null );
}
class ConcreteErrorHandler implements ErrorHandler {
protected $logger;
public function __construct( Logger $logger = null )
{
$this->logger = $logger ?: new VoidLogger();
}
public function handle( $errno, $errstr , $errfile = null , $errline = null , $errcontext = null )
{
echo "Triggered Error Handler";
$this->logger->log('An error occured. Some Logging.');
}
}
The handle()
method needs no further discussion. It's signature adheres to the needs of the set_error_handler()
function, and we make it sure by defining a contract.
The interesting part here is the constructor. We're typehinting a Logger
(interface) here and allow null to be passed.
<?php
interface Logger {
public function log( $message );
}
class ConcreteLogger implements Logger {
public function log( $message )
{
echo "Logging: " . $message;
}
}
The passed Logger
instance will get assigned to the corresponding property. However if nothing is passed an instance of a VoidLogger
is assigned. It violates the principle of DI, but it's perfectly fine in that case because we make use of a specific pattern.
The Null Object Pattern
One of your criteria was the following:
Note that the Logger may or may not be initiated, and the idea is that one would be able to easily define another Logger somehow.
The Null Object Pattern is used when you need an object with no behavior, but want to adhere to a contract.
Since we call the log()
method on the Logger in our ErrorHandler, we need a Logger
instance (we cannot call methods on nothing). But nobody forbids us to create a concrete implementation of a Logger which does nothing. And that's exactly what the Null Object pattern is.
<?php
class VoidLogger implements Logger {
public function log( $message ){}
}
Now, if you don't want have logging enabled, don't pass anything to the Error Handler during instantiation or pass a VoidLogger
by yourself.
Usage
<?php
$errorHandler = new ConcreteErrorHandler(); // Or Pass a Concrete Logger instead
set_error_handler([$errorHandler, 'handle']);
echo $notDefined;
To use your PSR Logger you just need to tweak the type hints and method calls on the the logger a bit. But the principles stay the same.
Benefits
By choosing this type of implementation you gain the following benefits:
- Easily swappable Loggers for the Error Handler
- Even easy swappable Error Handlers
- Loose Couplding (Decoupled logging from handling error)
- Easily extendable Error Handlers (you can inject other stuff, not only a logger)