Wednesday 6 August 2014

PHP's error "handling". Oh dear

G'day:
OK, so I am gonna side track into error-handling in PHP. During the writing of the previous article ("Looking at PHP's OOP from a CFMLer's perspective (part 1/?)") I needed to try to try/catch some stuff, and ended up having to learn a fair bit about how PHP implements error handling. Which is a bit of a horror story.

Initially it all seemed familiar... PHP has the usual try/catch/finally carry on:

<?php
// exception.php
try {
    throw new Exception("Exception Message");
}catch(Exception $e){
    echo "Message: [" . $e->getMessage() . "]<br>";
    echo "Code: [" . $e->getCode() . "]<br>";
    echo "File: [" . $e->getFile() . "]<br>";
    echo "Line: [" . $e->getLine() . "]<br>";
    echo "Trace: [" . $e->getTraceAsString() . "]<br>";
}finally{
    echo "After try/catch have completed<br>";
}

This outputs:

Message: [Exception Message]
Code: [0]
File: [d:\websites\www.scribble.local\shared\scratch\php\experiment\exceptions\exception.php]
Line: [4]
Trace: [#0 {main}]
After try/catch have completed


Cool.

One can also have custom exception objects (something CFML really ought to have):

Update:

I've added the work "objects" in the above sentence to clarify some... confusion.

<?php
// CustomException.class.php

class CustomException extends Exception {

    public function __construct($message, $detail, $code = 0, Exception $previous = null) {
        $this->detail = $detail;

        parent::__construct($message, $code, $previous);
    }

    public function getDetail(){
        return $this->detail;
    }

    public function __toString(){
        return "CustomException as a string ($this->code)";
    }

}

<?php
// customException.php

require "CustomException.class.php";

try {
    throw new CustomException("CustomException Message", "CustomException Detail", -1);
}catch(Exception $e){
    echo "Message: [" . $e->getMessage() . "]<br>";
    echo "Detail: [" . $e->getDetail() . "]<br>";
    echo "Code: [" . $e->getCode() . "]<br>";
}

throw new CustomException("Unhandled CustomException Message", "Unhandled CustomException Detail", -1);

Ouptut:
Message: [CustomException Message]
Detail: [CustomException Detail]
Code: [-1]

Fatal error: Uncaught CustomException as a string (-1) thrown in d:\websites\www.scribble.local\shared\scratch\php\experiment\exceptions\customException.php on line 14


This has a coupla cool things:
  • we can add more properties to the exception;
  • the __toString() method dictates what comes out on the screen when an uncaught exception is raised.
That's all wonderful. But what happens if the error condition is not one that's actually from a specifically-raised exception, eg:

<?php
// error.php

$dividend = $_GET["dividend"];
$divisor = $_GET["divisor"];


echo "Dividend: $dividend<br>";
echo "Divisor: $divisor<br>";

try {
    $result = $dividend / $divisor;
    echo "$dividend / $divisor = $result";
    
} catch (Exception $e){
    echo "Exception was caught: " . $e->getMessage();
}

One runs this thus: error.php?dividend=10&divisor=2, and the output in this case would be:

Dividend: 10
Divisor: 2
10 / 2 = 5


But what if one causes an error? Let's pass a zero as the divisor and cause a division-by-zero error:

Dividend: 10
Divisor: 0

Warning: Division by zero in d:\websites\www.scribble.local\shared\scratch\php\experiment\exceptions\error.php on line 12
10 / 0 =


Huh? But we've got a try/catch around that!

Well here's the thing. a try/catch in PHP only handles specifically-thrown exceptions. It doesn't handle errors. FFS, that's about the most stupid thing I have ever heard. What were they thinking?

There is a work-around though:

<?php
// trappedError.php

set_error_handler(function($errno, $errstr, $errfile, $errline, $errcontext){
    throw new Exception($errstr, $errno);
});

$dividend = $_GET["dividend"];
$divisor = $_GET["divisor"];

echo "Dividend: $dividend<br>";
echo "Divisor: $divisor<br>";

try {
    $result = $dividend / $divisor;
    echo "$dividend / $divisor = $result";
    
} catch (Exception $e){
    echo "Exception was caught: " . $e->getMessage();
}

set_error_handler() describes how to handle errors, rather than exceptions. And we can handle it by raising an exception instead. Done.

Not quite.

That doesn't deal with all errors ("oh of course it doesn't" I was thinking by now).

It does not deal with "fatal" errors. That's not "fatal" as in "me stabbing PHP in the face because it bloody deserves it", but just something like this:

<?php
// fatal.php
set_error_handler (function($errno, $errstr, $errfile, $errline, $errcontext){
    throw new Exception($errstr, $errno);
});

try {
    $doesNotExist = null;
    echo $doesNotExist->method();
} catch (Exception $e){
    echo "Exception was caught: " . $e->getMessage();
}
echo "Next line to process<br>";

This results in:

Fatal error: Call to a member function method() on a non-object in d:\websites\www.scribble.local\shared\scratch\php\experiment\exceptions\fatal.php on line 9


I actually think that far from being a "fatal" error, that's fairly innoccuous, and should be able to be remedied. We can suppress it, but we can't actually let processing continue:

<?php
// fatalHandled.php

register_shutdown_function(function(){
    $error = error_get_last();
    if (!isset($error)){
        return;
    }
    print_r($error);

    // log error or some other handling of it here
});

ini_set("display_errors", false);

try {
    echo "Before error<br>";
    $doNotExist = null;
    $doNotExist->method();
    echo "After error<br>";
} catch (Exception $e){
    echo "Exception was caught: " . $e->getMessage();
}
echo "End of file<br>";

When a fatal error occurs, processing is aborted, and the next thing that runs is the shutdown process. We can hook into this with register_shutdown_function(), and within that deal with fatal errors more tidily. Bear in mind that the shutdown callback is called at the end of every request, not just erroring ones, so check to see if there was an error before charging off and processing it.

Also note we need to switch error reporting off too, because the error is output before the shutdown handler is called. Sigh.

That's a lot of work for something most other language make rather easy. I seriously dunno what the language "designers" were thinking when they did this. Maybe I'm missing something really kewl about it. But from where I am currently sitting it just seems leaden.

And now back to looking at the OO stuff.

Righto.

--
Adam