www.voj-tech.net


Error Handling in vtCompose

It is important for any application to expect that things will go wrong at some point. In case of a simple website this could just be someone trying to access a non-existent page. In case of a more complicated web application we are talking about things like an unavailable external or system resource, say an unresponsive or incorrectly working remote server. In such situations PHP can report errors or throw exceptions both of which should be handled appropriately to alleviate the consequences. Let us have a look at the possibilities we have when using the vtCompose API to handle error states.

The All-Inclusive Boilerplate

Our script needs to start with including the vtCompose class loader, importing any classes and interfaces which will be referenced later and registering the class loader. The following code snippet imports all the classes and interfaces referenced throughout this article. One should cherry pick those imports of classes and interfaces which are actually required by the chosen solution.

<?php

require_once 'VTCompose/Autoloading/Autoloader.php';

use VTCompose\Autoloading\Autoloader;
use VTCompose\Data\DataAdapter;
use VTCompose\Data\DatabaseConnection\PostgreSql\Connection;
use VTCompose\Data\DatabaseModel\Database;
use VTCompose\Data\DatabaseModel\Schema;
use VTCompose\Enum;
use VTCompose\ErrorHandling\ErrorHandler;
use VTCompose\ErrorHandling\ExceptionHandler;
use VTCompose\ErrorHandling\IExceptionHandlerImplementor;
use VTCompose\ErrorHandling\IShutdownErrorHandlerImplementor;
use VTCompose\Http\Header\FieldName;
use VTCompose\Http\Request;
use VTCompose\Http\Response;
use VTCompose\Http\StatusCode;
use VTCompose\Logging\FileLogger;
use VTCompose\Logging\Severity;
use VTCompose\Sample\DatabaseModel\Schema as VTComposeSchema;
use VTCompose\Sample\ErrorHandling\Implementor;
use VTCompose\Sample\Web\ErrorHandlerFactory;
use VTCompose\Web\Application;
use VTCompose\Web\Context;
use VTCompose\Web\IRequestHandler;
use VTCompose\Web\IRequestHandlerFactory;

(new Autoloader())->register();

Turning PHP Errors into Exceptions

Probably the most basic use of the vtCompose error handling API is to register an instance of the VTCompose\ErrorHandling\ErrorHandler class as in the code example below. This way we make sure that any PHP error which is configured to be reported by the error_reporting php.ini directive causes a VTCompose\ErrorHandling\ErrorException to be thrown. Assuming the directive is set to a sensible value it is safe to say that we are not missing any error silently occurring in the background. As you can guess the ErrorHandler class wraps around the PHP set_error_handler() function.

(new ErrorHandler())->register();

It might be considered a good practice to register an instance of the ErrorHandler class since even vtCompose internals are written in such a way that they benefit from exceptions thrown where PHP errors would otherwise occur.

Handling PHP Errors at Shutdown

There is a way of extending the functionality of the ErrorHandler class used in the previous example. Certain error types cannot be handled with a user defined function set using the PHP set_error_handler() function. Particularly fatal run-time errors will not be turned into exceptions as they halt the execution of the script instead. Although it is not possible to recover from such errors we can still intercept them at PHP shutdown.

In the following example we define a class implementing the VTCompose\ErrorHandling\IShutdownErrorHandlerImplementor interface. If there has been an error the IShutdownErrorHandlerImplementor::handleError() method is called at PHP shutdown with the last occurred error details. In the example we only log the error to a file but if programming in a web environment we could also check whether there has been any output sent to the client already and if not we could display an error page. To use our implementation of the interface we pass an instance of it to the ErrorHandler constructor.

class MyShutdownErrorHandlerImplementor implements IShutdownErrorHandlerImplementor {

    private $logger;

    public function __construct() {
        $this->logger = new FileLogger('/var/log/my_app/shutdown_error');
    }

    public function handleError($message, $severity, $filename, $lineNumber) {
        $logMessage = 'Detected error at shutdown ' .
                '(message = "%s", severity = %d, filename = "%s", line number = %d).';
        try {
            $this->logger->log(Severity::ERROR, $logMessage, $message, $severity, $filename, $lineNumber);
        } catch (Throwable $exception) {
            // swallow any exception
        }
    }

}

(new ErrorHandler(new MyShutdownErrorHandlerImplementor()))->register();

Note that in PHP 5.6 (using vtCompose 0.1.2) we would catch an instance of the PHP Exception class instead of an instance of the PHP 7 Throwable interface when logging the error.

Handling Uncaught Exceptions

If we turn PHP errors into exceptions as we have done so above, we need to either remember to use try/catch/finally blocks as necessary or be prepared that we will end up with uncaught exceptions. The VTCompose\ErrorHandling\ExceptionHandler class comes in handy when dealing with the latter.

The class is a thin wrapper of the set_exception_handler() PHP function and its constructor takes an instance of a class implementing the VTCompose\ErrorHandling\IExceptionHandlerImplementor interface as a parameter. In case of an uncaught exception the IExceptionHandlerImplementor::handleException() method is called and we have the last chance to take any action before the script execution stops. In our example we present a simple implementation of the interface which merely logs the exception to a file. Again, if programming in a web environment we could also check whether there has been any output sent to the client already and if not we could display a friendly error page.

class MyExceptionHandlerImplementor implements IExceptionHandlerImplementor {

    private $logger;

    public function __construct() {
        $this->logger = new FileLogger('/var/log/my_app/uncaught_exception');
    }

    public function handleException(Throwable $exception) {
        try {
            $this->logger->log(Severity::ERROR, $exception);
        } catch (Throwable $anotherException) {
            // swallow any additional exception
        }
    }

}

(new ExceptionHandler(new MyExceptionHandlerImplementor()))->register();

Note that in PHP 5.6 (using vtCompose 0.1.2) the IExceptionHandlerImplementor::handleException() method must take an instance of the PHP Exception class instead of an instance of the PHP 7 Throwable interface and, again, we would catch any additional exception as an Exception instance instead of a Throwable instance when logging the exception.

The Built-In Shutdown Error and Uncaught Exception Handler

vtCompose comes with a built-in implementation of both the IShutdownErrorHandlerImplementor and the IExceptionHandlerImplementor interfaces. It is the VTCompose\Sample\ErrorHandling\Implementor class. The implementation is very specific and not for everyone hence the namespace VTCompose\Sample. Both of the handlers perform three operations; they display a custom error page if no output has been sent to the client, they log the PHP error or the exception (typically to a file, this depends on the injected VTCompose\Logging\ILogger implementation) and finally they log the same into designated database tables. The required table definitions are distributed with vtCompose. At the moment it is only possible to connect to a PostgreSQL database with vtCompose and so the schema has been tested likewise in PostgreSQL only. It should be stressed that this error handling implementation is intended solely for a web environment as it attempts to display an error page. Also note that the output_buffering php.ini directive should be set to 'Off' in order for the detection of any output sent to the client to work correctly. Let us go through an example of how the implementation can be used. The example involves creating a database model, see the Data Access page for more on accessing data.

The database tables for logging PHP errors detected at shutdown and uncaught exceptions are the shutdown_error_log and uncaught_exception_log tables. They need to be created in a schema called vtcompose separating them from the rest of your application schema. As already mentioned the table definitions are distributed with vtCompose and so all we have to do is to attach the vtcompose schema model to our database model MyDatabase along with our application schema model MySchema. When creating an instance of the Implementor class we pass a few arguments to the constructor; our DataAdapter instance, an ILogger instance, a path to a static HTML file containing the error page markup and an associative array specifying HTTP headers to accompany the error page when sent to the client.

final class SchemaId extends Enum {
    const VT_COMPOSE = 0;
    const MY_SCHEMA = 1;
}

class MySchema extends Schema {

    public function getName() {
        return 'my_schema';
    }

    protected function createTables() {
        return [
            // my_schema table definitions...
        ];
    }

}

class MyDatabase extends Database {
    protected function createSchemas() {
        return [
            new VTComposeSchema($this, SchemaId::VT_COMPOSE),
            new MySchema($this, SchemaId::MY_SCHEMA),
        ];
    }
}

$connection = new Connection();
$databaseModel = new MyDatabase();
$dataAdapter = new DataAdapter($connection, $databaseModel);

$errorPageHeaders = [
    FieldName::CONTENT_TYPE     => 'text/html; charset=UTF-8',
    FieldName::CONTENT_LANGUAGE => 'en',
];

$logger = new FileLogger('/var/log/my_app/error');
$errorPageFilename = '/var/local/my_app/error.html';
$implementor = new Implementor($dataAdapter, $logger, $errorPageFilename, $errorPageHeaders);
(new ExceptionHandler($implementor))->register();
(new ErrorHandler($implementor))->register();

$connection->setConnectionString('dbname=my_app');
$connection->open();

Handling HTTP Errors

The last example we demonstrate here solves a different problem. In this case rather than deal with an error having originated internally in our PHP application we use vtCompose to handle redirects based on the ErrorDocument Apache HTTP Server directive. One possible scenario is that a user attempts to access a non-existent resource and a directive such as the ErrorDocument 404 /index.php directive happens to be configured on the server. It is possible to use the index.php file since a single vtCompose script is capable of serving both successful and error responses. The HTTP server runs PHP having set a few environment variables including the REDIRECT_STATUS variable. When vtCompose detects that the variable is set to a value other than 200 it checks whether an instance of a class implementing the VTCompose\Web\IErrorHandlerFactory interface has been specified and uses it to create a special error-handling instance of the VTCompose\Web\IRequestHandler interface if it has.

In the code example you can see that we instantiate the VTCompose\Sample\ErrorHandlerFactory class passing the name of a class implementing the IRequestHandler interface as an argument.

class MyRequestHandler implements IRequestHandler {
    public function handle(Request $request, Context $context) {
        $response = new Response(StatusCode::OK);
        // standard request handling...
        return $response;
    }
}

class MyErrorHandler implements IRequestHandler {
    public function handle(Request $request, Context $context) {
        $statusCode = $context->getHttpStatusCode();
        $response = new Response($statusCode);
        $response->setBody(<<<HTML
<!DOCTYPE html>
<head>
    <meta charset=UTF-8>
    <title>Error</title>
</head>
<body>
    <h1>An error occurred</h1>
    <p>Status code: $statusCode</p>
</body>
HTML
            );
        return $response;
    }
}

class MyRequestHandlerFactory implements IRequestHandlerFactory {
    public function createHandler($path, $httpMethod, $host) {
        return new MyRequestHandler();
    }
}

(new Application(new MyRequestHandlerFactory(), new ErrorHandlerFactory('MyErrorHandler')))->run();

We have mentioned some database table definitions related to error handling are distributed with vtCompose. You might notice that there is another table definition distributed with the framework. This is the web_request_handlers table and it serves as a data source for a built-in implementation of the IRequestHandler interface which attempts to look up any requested URI in the table. It is not necessary to create the table if you do not intend to use this IRequestHandler implementation.