RSS Feed

Peter Goodman's blog about PHP, Parsing Theory, C++, Functional Programming, Applications,

Taking Advantage of Exceptions in PHP5

I considered the following hack to be interesting and possibly useful at the time of writing this article; however, under no circumstances should the code below be used or thought of as a practical solution to the provided problem.

If you've never used PHP5 exceptions then it's time to start. They are a great way of handling specific types of errors uniformly and allow you to handle/fix/bypass problems that could otherwise cripple your script.

An example use case for custom exceptions is rollbacks for database transaction queries. If that sounded like jibberish, some databases allow what are called transactions. You are allowed to start them, save them, and commit them. They wrap around database queries, essentially encapsulating them, and if the queries fail, allow you to undo everything that the queries did. But back to the exceptions...

Using Third-Party Code

Here is a real-world example that might seem too convenient but is entirely possible. Lets assume that we are using a third-party framework or database abstraction layer. We don't necessarily know anything about its inner workings, but we do know that it will throw an exception if any database queries fail. Now, assume that we are working on a large-scale application and that there are a a few (sets?) of queries that affect a lot of the database, and that if they were to fail, the results would be disastrous (for example: locking a database table and not unlocking it).

Well, there are a few ways to approach the problem of one of the queries failing and having the third-party code throw an exception. Here's the first way--in pseudo-php/framework code--that we might resolve any problems. This way is clean and simple and works well with the transactions.

try {
    $db->beginTransaction('mission_critical');
    $db->query("UPDATE ...");
    $db->commitTransaction('mission_critical');

} catch(DatabaseQueryException $e) {
    $db->rollbackTransaction('mission_critical');
}

If an error occured then an exception will be thrown and caught by our try ... catch statement and the database will be saved! Unfortunately, this presupposes that the exception is caught in the first place. What if the roles in this were reversed? What if instead of being the programmer taking advantage of this amazing third-party code, you are actually the developer of said code?

Making Your Own Code

Now that we're the developer of this framework/database abstraction layer, we have absolutely no control over how our functions are called. However, we can assume the worst from our users and help them not destroy their database! This might be a bit too precautionary for your liking, and you won't necessarily like this solution.

We're going to make the assumption that all of the code above is the same with the exception that there are no try ... catch statements. The coder has cleverly put their mission critical queries into a transaction, but mistakenly forgotten to account for possible errors in the queries. Well, it's time to go into our database abstraction layer!

public function query($sql) {
    if(!$this->mysqli->query($sql)) {
        throw new DatabaseQueryException($this);
    }
}
public function beginTransaction($id) {
    $this->transactions[] = $id;
    
    // begin a transaction
    $this->mysqli->autocommit(FALSE);
}
public function commitTransaction() {
    if(!empty($this->transactions)) {
        array_pop($this->transactions);
    
        // commit the transaction
        $this->mysqli->commit();
    }
}

So do you see where I'm going with this? Hint: I'm passing "$this" to DatabaseQueryException when I instanciate it. Another thing to note is that I create a stack of SQL transactions. Note: With MySQLi, the $id isn't actually supported for transactions, but for other database layers it could be. That is why I have put it there.

So, what's immediately obvious is that when be begin a transaction, we push a variable onto a $transactions array, effectively keeping track of which transactions are currently not committed. When we commit a transaction, we pop the last element off of the $transactions array, saying that we've committed it. Now, what happens when an error happens in query() and the DatabaseQueryException is thrown (and maybe caught?). Lets take a look at the definition for DatabaseQueryException and find out ;)

class DatabaseQueryException extends Exception {
    private $db;
    public function __construct($db, $code) {
        $this->db = $db;
        
        // pass an error string and code to 
        // Exception/parent class so everything
        // is properly set.
        parent::__construct($db->error(), $code);
    }
    
    // tear down / clean up
    public function __destruct() {

        // rollback any transactions that might have 
        // failed/ be unfinished
        foreach($this->db->transactions as $id) {
            $this->db->rollbackTransaction($id);
        }
        
        // remove any last references to the db object, 
        // causing its destructor to be called in the
        // process.
        unset($this->db);
    }
    
    // make sure the destructor is called if the exception 
    // isn't caught
    public function __toString() {
        $this->__destruct();
        
        return parent::__toString();
    }
}

It's a pretty straightforward class, the trick to it is DatabaseQueryException::__toString(). If you define a destructor in an exception, you will quickly find out that it's not called unless the exception is caught. The obviously solution is to explicitly call it from some function that we absolutely know will be called if an exception isn't caught. That function is Exception::__toString().

Why Use a Destructor in the First Place?

Using an exception's destructor as a clean up method is useful because a) it is automatically called when an exception is caught, and b) it is called at the end of a catch statement. Being at the end of a catch statement can give the programmer certain advantages. For example, within the catch statement you have an instance of the exception readily available. Given this you now have an opportunity to do stuff with it. You can pass it information, tell it to do things, and after all of that it will destruct as expected.


Comments


Comment