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.
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: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
where
$errorHandler
is the object andhandle
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.The
handle()
method needs no further discussion. It's signature adheres to the needs of theset_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.The passed
Logger
instance will get assigned to the corresponding property. However if nothing is passed an instance of aVoidLogger
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:
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 aLogger
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.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
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: