PHP Sadness

(<5.5) No support for finally-type error handling

Update 2013-01-07: As of PHP 5.5, there is support for finally.

Update 2012-08-06: It looks like there is a proposal to finally add finally!

In <5.5, PHP has no finally mechanism, which would allow programmers to register some code to be run as cleanup before leaving a try block, regardless of how it is exited (exception, return, or normally). While PHP allows for an alternative approach through destructors, such a mechanism is still valuable when working with objects that do not correctly support the Resource Acquisition Is Initialization model.

This is a known missing feature, although the bug report is marked "closed".

I submitted an ugly workaround hack on 2011-12-05; you are free to use it in your own programs (use the slightly updated version at the bottom of this page). It works by taking some callable and firing it when a scope is exited (or a point in the scope is reached) by creating a local object and waiting for it to be destroyed. This is not a substitute for a real language feature!

To use the hack, load the class (below), then put your finally code in a function and wrap your try/catch/... blocks like this:

$finally = new Finally("callback_name");
try {
  // ...
} catch (Exception $e) {
  // ...
}
$finally->done();

If the wrapped section of code completes normally, the $finally->done() call is reached, which invokes the finally code (callback_name()) and disarms itself. However, if the scope is exited abnormally, $finally is destroyed during stack cleanup, and the finally code is called immediately, before the next layer of the stack is even reached.

For example, suppose you have the following finally callback:

// With no lambda functions <5.3, we do this instead :(
// You can define and supply any arguments you like; register them in the Finally constructor.
function example_finally($example_name)
{
  print "Finally was called from $example_name!\n";
}

Here are three ways (1 and 2 are the same) this finally code can be triggered:

// This example leaves the scope normally (after exiting the try{...} block normally).
function example1()
{
  $finally = new Finally("example_finally", array("example1"));
  try
  {
    print "Exit try{...} normally\n";
  }
  catch (Exception $e)
  {
    print "Caught exception in example1()!\n";
  }
  $finally->done();
}

// This example leaves the scope normally (after exiting the try{...} block with a caught exception).
function example2()
{
  $finally = new Finally("example_finally", array("example2"));
  try
  {
    print "Throwing an exception!\n";
    throw new Exception();
  }
  catch (Exception $e)
  {
    print "Caught exception in example2()!\n";
  }
  $finally->done();
}

// This example leaves the scope with an uncaught exception.
class OtherException extends Exception {}
function example3()
{
  $finally = new Finally("example_finally", array("example3"));
  try
  {
    print "Throwing an exception!\n";
    throw new Exception();
  }
  catch (OtherException $e)
  {
    print "Caught exception in example3()!\n";
  }
  $finally->done();
}

// This example leaves the scope by returning.
function example4()
{
  $finally = new Finally("example_finally", array("example4"));
  try
  {
    print "Returning!\n";
    return;
  }
  catch (Exception $e)
  {
    print "Caught exception in example4()!\n";
  }
  $finally->done();
}

If you invoke these example functions,

try {
  print "\nexample1():\n"; example1();
} catch (Exception $e) { print "...exception fell out!\n"; }

try {
  print "\nexample2():\n"; example2();
} catch (Exception $e) { print "...exception fell out!\n"; }

try {
  print "\nexample3():\n"; example3();
} catch (Exception $e) { print "...exception fell out!\n"; }

try {
  print "\nexample4():\n"; example4();
} catch (Exception $e) { print "...exception fell out!\n"; }

they produce:

example1():
Exit try{...} normally
Finally was called from example1!

example2():
Throwing an exception!
Caught exception in example2()!
Finally was called from example2!

example3():
Throwing an exception!
Finally was called from example3!
...exception fell out!

example4():
Returning!
Finally was called from example4!

Note especially the order of events for example 3: throw, finally, catch.

Here is the Finally class implementation; it is all you need to do this in your own code:

class Finally
{
  private $_callback = NULL;
  private $_args = array();

  function __construct($callback, array $args = array())
  {
    if (!is_callable($callback))
    {
      throw new Exception("Callback was not callable!");
    }

    $this->_callback = $callback;
    $this->_args = $args;
  }

  public function done()
  {
    if ($this->_callback)
    {
      call_user_func_array($this->_callback, $this->_args);
      $this->_callback = NULL;
      $this->_args = array();
    }
  }

  function __destruct()
  {
    $this->done();
  }
}