Php Exception Router Implementation

Essentially, I want an exception catcher that will let me declare errors in my library, so that other developers get meaningful error reports, rather than the generic ones provided by PHP. I wrote about it here, but I've thought about it more since then.

Use Case

RedbeanPhp sometimes throws an error with the message Call to a member function find() on null, which means no database has been connected to redbean. I believe its triggered by the self::$finder->find() call in this method:

public static function find( $type, $sql = NULL, $bindings = array(), $snippet = NULL )
{
    if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet );
    return self::$finder->find( $type, $sql, $bindings );
}

This is okay, anybody can follow the stack trace, dig into redbean a little, and go "OH I didn't connect the DB.". But its burdensome.

Simple, cumbersome solution

An alternate would be to rewrite the return line with a try/catch that adds details like so:

try {
    return self::$finder->find( $type, $sql, $bindings );
} catch (\Error $e){
    echo "\n\nYou must connect the database.\n";
    throw $e;
}

But try/catch likely adds overhead (something I should benchmark), and its a lot of boilerplate. There will be many calls to self::$finder->find() across the codebase, and now thats QUITE a lot to manage.

Proposed Exception Catcher

There's one consistent thing here: The message will always be Call to a member function find() on null, which always means "you need to connect the databse". There are many ways to implement this, but here's my current idea:

<?php
class RedbeanErrors extends \Tlf\ExceptionRouter {
    
    protected $errors = [
        0 => [
            'message'=>'Call to a member function find() on null',
            'report'=>'You need to call `::setup()` to connect the database. See https://redbeanphp.com/index.php?p=/connection',
        ],
        1 => [ /** another exception I specifically handle in my redbean wrapper library RDB */
            'message'=>'Array may only contain OODBBeans',
            'report'=>'You added a raw value to an ownParamList[]. You MUST only add beans to these.'
        ]
    ];
}

And that RedbeanErrors class would define several other errors. Traits could be used to combine multiple lists of errors if this one array grows taller than a giraffe.

How to use

The user of Redbean could wrap their entire program in a try/catch and call the exception catcher. Example:

require_once(__DIR__.'/vendor/autoload.php');
/** this responds to a request, using a framework I built */
try {
    $liaison = new \Liaison();
    $package = new \Lia\Package(__DIR__.'/MyWebsite/');
    // R::setup(); 
    $liaison->deliver(); // responds to $_SERVER['REQUEST_URI']
} catch (\Throwable $t){
    \RedbeanErrors::throw($t);
}

This will fail because OOPS, the ::setup() line is commented out. The \Tlf\ExceptionRouter parent class will load the $errors, and match the messages & give the report that's much more human friendly, before re-throwing.

Additional Features

We might also want to respond to specific calling classes or something.

<?php
class RedbeanErrors extends \Tlf\ExceptionRouter {
    
    protected $errors = [
        0=>[
            // just some ideas
            'called_from_file'=>'relative-path/to-file.php',
            'called_from_class'=>'MyBigErrorFilledClass',
            'called_method'=>'OneThatThrowsALot',
            'report'=>'Something something, important message'
        ]
    ];

    protected function onUnhandledError($message, $throwable, array $betterOrganizedErrorInfo){
        echo "Woohoo you broke stuff!";
        exit;
    }
}

We also might care about checking between a line_start and line_end if not in a class.

Conclusion, Benefits

RedbeanPhp would have a single class that lists all of its major errors. Users of Redbean would have detailed feedback when they mess up implementation. Less searchable documentation is needed, as error reports can guide new users.

The dependency on \Tlf\ExceptionRouter could be optional, so Redbean ships with the RedbeanError class, but doesn't require the bloat of my (non-existent) ExceptionRouter library. For production, this would all be pretty easy to disable, which means benefits to performance & a reduction in your carbon footprint.

Overall, I think this would make development, especially when integrating multiple dependencies, far far easier.

Comment and discuss on twitter

License

This blog post is mine. The ideas and code within it are free to use & claim as your own. If you make it, I hope its free and open source, but that's totally up to you. I also hope you attribute me, but you don't have to.