Best way to pass a class into a callback function

509 views Asked by At

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?

1

There are 1 answers

0
thpl On BEST ANSWER

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)