I am using a PSR-3 logging class, and I am attempting to use it in conjunction with set_error_handler()
. My question is how do I properly "grab" the logging object?
Quick Example:
My ErrorHandler.php
:
set_error_handler(function ($errno, $errstr , $errfile , $errline , $errcontext) {
// This error code is not included in error_reporting
if (!(error_reporting() & $errno)) {
return;
}
$logger->log(/* How? */);
});
My Logger.php
:
class Logger extends PsrLogAbstractLogger implements PsrLogLoggerInterface {
public function log($level, $message, array $context = array()) {
// Do stuff
}
}
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.
It occurs to me that I have at least two options, which would be simply use a global variable called $logger
or something similar, and use that (even though the Logger
object will not be initialized in the global scope in my particular example), or to use a singleton pattern "just this one time", where I would define a static method inside of the Logger
class, so that I could use something such as:
$logger = Logger::getInstance();
Although I have seen a lot of very harsh things said about the Singleton pattern, some even calling it an "Anti-Pattern". I am using dependency injection (as nicely as I can) for the rest of the project.
Am I missing another option, or is there a "Right" way to do this?
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.
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 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.
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.
<?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.
By choosing this type of implementation you gain the following benefits: