Tuesday 23 June 2015

PHP 7: PHP starts to play catch-up with error- / exception-handling

G'day:
I'm not doing so well on the PHP 7 testing so far. CFML stuff keeps getting in the way.

I did start having a lot at one of the better improvements to PHP 7 the other day: improvements to exception handling. Exception handling in PHP has always been a bit "odd" (OK, I mean "shit") in my opinion. But it's slightly de-shit-ified itself in PHP 7.

Exception handling in CFML

As a benchmark, I'll use CFML's exception handling as an example. I touched on this the other day ("CFML: spending pointless time looking and exceptions and errors and throwables"), but the bottom line is that basically if a "problem" occurs at runtime, CFML reports it as an exception, and it's catchable. Not rocket science.

Exception handling in PHP (prior to 7)

Actually I can't be that clear about how exception handling in PHP currently "works", as it's all a bit impenetrable (see "PHP's error "handling". Oh dear" for my first foray into it).

The short version is PHP has different levels of "things going wrong", only very few of which are "exceptions", and accordingly are catchable. Some just make the request die a horrible death, despite seeming fairly innocuous, and clearly a run-time exception sort of situation. Here's an example:

// errorMishandling.php

class Person {}

class PersonFactory {
    public static function getPersonFromId($id){
        if ($id > 0) {
            return new Person();
        }
    }
}

function doSomethingToPerson(Person $x){
    return true;
}

$person = PersonFactory::getPersonFromId($_GET['id']);

try {
    $result = doSomethingToPerson($person);
    echo 'Process ran OK';
} catch(Exception $e) {
    echo 'Exception caught';
}
finally {
    echo 'In finally block<br>';
}

echo 'After try/catch/finally<br>';

Here I have the following:

  • A stubbed Person class: I just need to create an object (or not, depending on scenario).
  • A factory that either returns a Person class, or not. Perhaps emulating whether the relevant Person object is found in storage, or something.
  • A function which takes a Person and does something with it
  • So I get a Person based on an ID passed on the URL
  • And whatever comes back from the PersonFactory, I pass to afore-mentioned function.
  • And attempt to intercept any problems.
When I run this on PHP (as-is, without the ID being passed):


Notice: Undefined index: id in D:\php\php.local\www\experiment\7\exceptionHandling\errorMishandling.php on line 20

Catchable fatal error: Argument 1 passed to doSomethingToPerson() must be an instance of Person, null given, called in D:\php\php.local\www\experiment\7\exceptionHandling\errorMishandling.php on line 23 and defined in D:\php\php.local\www\experiment\7\exceptionHandling\errorMishandling.php on line 16


Two things of note here:
  • I get a kinda error reported that I am trying to use the ID param even though it doesn't exists... but processing keeps going...
  • ... and then PHP also fails to let me catch the (supposedly catchable!) error, and now the code bombs out.
I've got no idea why PHP would think that a way to handle something going wrong is just to put a message on the screen then continue.

And even less bloody idea why it would not let me catch something as inconsequential as a type-checking error. Let's remember that PHP is supposedly a dynamically and loosely typed language, so it should kinda expect this sort of thing.

Now I'm not suggesting my code should have worked, but it should have:
  • bombed when I tried to use a variable that didn't exist;
  • not bombed when I tried to use a variable of the wrong type.

It's got this about as wrong as it possibly could.

Oh, btw, if I pass the ID on the URL as 0 I still get the latter error. If I give it a positive value (so it passes the conditional), the code runs fine.

Note that I can deal with the "catchable fatal error", but - ironically - not with a catch. But by horsing around with PHP's other error "handling" mechanism: set_error_handler(). But I dunno whether that makes it better or worse, TBH.

PHP 7 kinda deals with this

PHP 7 has added a type of catchable exception "EngineException". I think it's completely misnamed as it's not something going wrong with the engine at all, but still: PHP is perhaps the model example of "naming things is hard", so this figures.

Here's an augmented version of that try/catch block:

try {
    $result = doSomethingToPerson($person);
    echo 'Process ran OK<br>';
} catch(Exception $e) {
    echo 'Exception caught<br>';
} catch (EngineException $e){
    echo 'EngineException caught<br>';
} finally {
    echo 'In finally block<br>';
}

If I switch to running the code on PHP 7, I now get:

EngineException caught
In finally block
After try/catch/finally


Yay!

Kinda.

What bamboozles me is that despite being called an "EngineException", which kinda suggests it's a subclass of "Exception"... it's not. I cannot catch an EngineException in a catch block expecting an Exception. Well done on that count, PHP 7.

Update:

This state of affairs has been improved in Alpha 2. There is now a Throwable class which all Exceptions and Errors extend. The situation raised here no longer throws an EngineException, but a TypeError. Both TypeError and Throwable are catchable. This is all fairly reasonable. Now I just hope they also include Warnings as a type of Catchable at some point.


Still: at least one doesn't have to horse around with set_error_handler() to deal with these innocuous runtime errors now. One just needs to have two catch statements instead.

Now I'm sure this was all done as a band-aid solution for PHP's exception handling, as fixing it properly would cause backwards compat issues, but I'm sure I've read PHP takes the position of not needing to preserve backwards compat on full version releases, so they coulda just dealt with this properly could they not? At the very least they could have made EngineException extend Exception, and maybe have a php.ini switch to vary the behaviour for ppl who might have problems with it, perhaps?

Well I think - despite my griping - this is an improvement to PHP 7. But not as much as it ought to have been.

--
Adam