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
-
You're right that error recovery should not be the responisibility of exceptions. Exceptions are essentailly a way of triggering and then handling triggered predictable errors and their responsibilities should not extend to modifying the state of the code that they are reporting. Unfortunately PHP does not have a finally or else clause for exceptions and therefore if an exception is triggered there is no way to clean up after it if it's not caught! The above hack breaks the rules so to speak in order to allow the missing functionality.
posted by Peter Goodman on Aug 25, 2007 at 8:13pm -
Hm... Could error/exception recovery be seen as a responsiblity of an exception object? Recovery/Error handling could be done by a SecurityFacade hiding a subsystem and catching its exceptions to handle them... and maybe it involves something like RecoveryStrategy objects. The latter could be composed in an Exception object, that is, the exception object provides recovery solutions (methods)? Doing recovery silently in the destructor is not an expected behaviour... but I'm not sure here...
posted by Andre on Aug 22, 2007 at 12:19pm -
calling __destruct() normally will leave the object in an undefined state, i.e. parent::__toString() may no longer be available after that the destructor was invoked it is better to introduce a second method that will do the cleanup rather than the destructor, the destructor may always rely on the second method in order to do the clean up on destruction of the instance
posted by Carsten Klein on Mar 12, 2008 at 4:43pm -
Great advice.
posted by Peter Goodman on Dec 28, 2008 at 4:07pm -
Given that it's unlikely that you'd create instances of exceptions that you aren't going to throw, why not put the rollback code in __construct() instead? That way, if the exception is dealt with by a handler registered via set_exception_handler() which doesn't use __toString() (for example, it just prints a simple error page and bails—not that I'd recommend that, but the developer might not have got as far as implementing the exception logging part of the handler yet), the rollback code is still executed. I'll grant, putting it in the constructor is less elegant, but it seems more likely that __toString() would never be called than an exception class instance being constructed in a scenario other than it about to be thrown—and given that you control the code throwing AND the exception's implementation itself, but DON'T control the code handling the exception, putting it in the constructor seems the safest choice.
posted by Mo on Dec 29, 2007 at 9:40pm -
Why not put the rollback code in the destructor of your database abstraction layer? You're already keeping a stack of open transactions: if the destructor is called (the object is going out of scope) and there're any open transactions, the sensible thing is to rollback all those transactions and then throw an exception telling about the fact.
posted by Mikko Rantalainen on Jul 6, 2009 at 6:45am
That way any exception anywhere (not only exceptions thrown by your database layer) automatically rollback the transactions as required. -
I think I should simply put a message on this blog post for people to ignore it. I thought it was a clever hack at the time of writing it; however, in retrospect it is terrible advice.
posted by Peter Goodman on Jul 6, 2009 at 8:00am
Comment
