Type-safe callables in PHP by interfaces with __invoke()

The type system in PHP has become safer each version since PHP 7.0, by using a strict type declaration on top of the PHP file. As a newly-developed feature, it doesn’t come with the billion-dollar mistake of allowing NULL for variables declared with a class type, unless you allow it specifically.

However, if a function need to accept a callable which accepts parameters of a specific type and returns a specific type, you cannot do it with callable type declaration because there is no way to add type information into the callable declaration.

Luckily, there is a magic function called __invoke(). It can turn any object into a callable, and as a member function, you can add type declaration into it, which make the object only callable with the specified parameters.

For example, if you are writing an exception handler factory which need a third-party function to convert a Throwable into a ResponseInterface to produce a handler, instead of writing this:

function make_exception_handler(callable $get_response) : callable {
    return function (Throwable $e) use ($get_response) {
        // log the exception
        $response = $get_response($e);
        // output the response
    };
}

You can write the following instead:

interface ExceptionResponseFactoryInterface {
    public function __invoke(Throwable $exception) : ResponseInterface;
}

function make_exception_handler(ExceptionResponseFactoryInterface $get_response) : callable {
    return function (Throwable $e) use ($get_response) {
        // log the exception
        $response = $get_response($e);
        // output the response
    };
}

As you can see, there is absolutely no change in the function body, but the expectation is now clearly indicated in the function signature: get a Throwable and return a ResponseInterface. Now the code can be statically type-analysed by an IDE.

However, a caveat is that, you can’t directly pass anonymous functions into the function, instead, you must make an object implementing the interface instead. For example:

set_exception_handler(
    make_exception_handler(
        new class implements ExceptionResponseFactoryInterface {
            public function __invoke(Throwable $exception) : ResponseInterface {
                // do stuff here
                return new Response();
            }
        }
    )
);

Or use a generic wrapper class to convert non-type-safe legacy code:

class CallableExceptionResponseFactory implements ExceptionResponseFactoryInterface {
    private $callable;
    public function __construct(callable $callable) {
        $this->callable = $callable;
    }
    public function __invoke(Throwable $exception) : ResponseInterface {
        return $this->callable($exception);
    }
}

set_exception_handler(
    make_exception_handler(
        new CallableExceptionResponseFactory('get_response_from_exception')
    )
);

By using the above method, you can do type-safe functional programming in PHP, despite not having type signatures for callables.

Leave a Reply

Your email address will not be published. Required fields are marked *