Wednesday, December 9, 2009

Cleaning/restoring using helper objects

Just wanted to show all of you who use OO PHP a technique that is quite familiar to old-time C++ programmers, but generally not so well known in the PHP world (think auto_ptr). This technique allows for much cleaner code in scenarios where you have to allocate a resource (or change global state) and have to cleanup before exiting the (function/method) scope.

I first applied this when I had a method in a class that needed to execute a function that might create an error condition that I wanted to intercept because I needed to do some stuff before letting the exception escape. The classic PHP approach would be something like this:

The problem is that as written the error handler will not be restored if we (re) throw the exception. So we fix that:

But now we are starting to repeat the code. So here is what it looks like with the alternate, more object oriented technique:

So, where did the 'restore_error_handler' go? Now it won't be restored at all! Not so. Lets have a look at the ErrorHandler class:

Now, if we look at the last version of the 'dosomething' function above, you will notice that whether the function ends by throwing an exception, or because it returns normally, the '$handler' variable will go out of scope and will be destructed. The class' destructor function takes care of restoring the previous state. This approach allows you to forget about when/where to let go/restore of resources.

The technique can be generalized to any other kind of object that needs to clean up after itself. If you already have existing objects, make sure you have an appropriate '__destruct' method. The above illustrates how you can introduce some new objects to encapsulate create/restore behavior for non-object type functions.

WARNING PHP makes no guarantees about the order in which variables that go out of scope get destructed. This is generally not an issue, but if you were to, for example, create two handlers like the above in the same scope, they may get broken down in the opposite order from what you expect. Since 'restore_error_handler' carries no context, it doesn't matter in this case: as long as it gets done twice. It may not be true of other scenario's. Take this example:

Lets assume the error_reporting level is 0 before we execute 'test()'. Creating '$er1' will set it to 'E_ALL' (and remember 0), and then creating '$er2' will set it to 'E_WARN' (and remember 'E_ALL'). So far so good. We do some work and exit. If destruction happens in the order '$er2' and then '$er1' things are peachy and we end up with 0 as the error_reporting level. If destruction happens the other way around though, we end up with an error_reporting level of 'E_ALL'!

One simple way of getting around this problem is to have each object that implements this behavior hold a reference to other objects that should be destructed later:

Now '$er2' is holding a reference to '$er1'. If PHP wants to destruct '$er1' it founds it has a reference count of 2, so it is not considered (yet), but its count is reduced to 1. '$er2' has a reference count of 1, so it's '__destruct' function is called. It does what it needs to do, and then unsets the reference(s) it holds. This causes the reference count of '$er1' to go from 1 to 0 and '$er1' is finally destructed.

No comments: