How to Detect Errors in PHP? Well, that's tricky…
Among the top 5 monstrous quirks of PHP certainly belongs the inability to determine whether a call to a native function was successful or resulted in an error. Yes, you read that right. You call a function and you don’t know whether an error has occurred and what kind it was. [perex]
Now you might be smacking your forehead, thinking: surely I can tell by the return value, right? Hmm…
Return Value
Native (or internal) functions usually return false
in case of
failure. There are exceptions, such as
"json_decode":http://php.net/manual/en/function.json-decode.php
,
which returns null
if the input is invalid or exceeds the nesting
limit, as mentioned in the documentation, so far so good.
This function is used for decoding JSON and its values, thus calling
json_decode('null')
also returns null
, but as a
correct result this time. We must therefore distinguish null
as a
correct result and null
as an error:
$res = json_decode($s);
if ($res === null && $s !== 'null') {
// an error occurred
}
It's silly, but thank goodness it's even possible. There are functions,
however, where you can't tell from the return value that an error has occurred.
For example, preg_grep
or preg_split
return a partial
result, namely an array, and you can't tell anything at all (more in Treacherous Regular
Expressions).
json_last_error & Co.
Functions that report the last error in a particular PHP extension. Unfortunately, they are often unreliable and it is difficult to determine what that last error actually was.
For example, json_decode('')
does not reset the last error flag,
so json_last_error
returns a result not for the last but for some
previous call to json_decode
(see How to encode and decode JSON in
PHP?). Similarly, preg_match('invalidexpression', $s)
does not
reset preg_last_error
. Some errors do not have a code, so they are
not returned at all, etc.
error_get_last
A general function that returns the last error. Unfortunately, it is extremely complicated to determine whether the error was related to the function you called. That last error might have been generated by a completely different function.
One option is to consider error_get_last()
only when the return
value indicates an error. Unfortunately, for example, the mail()
function can generate an error even though it returns true
. Or
preg_replace
may not generate an error at all in case of
failure.
The second option is to reset the “last error” before calling our function:
@trigger_error('', E_USER_NOTICE); // reset
$file = fopen($path, 'r');
if (error_get_last()['message']) {
// an error occurred
}
The code is seemingly clear, an error can only occur during the call to
fopen()
. But that's not the case. If $path
is an
object, it will be converted to a string by the __toString
method.
If it's the last occurrence, the destructor will also be called. Functions of
URL
wrappers may be called. Etc.
Thus, even a seemingly innocent line can execute a lot of PHP code, which may
generate other errors, the last of which will then be returned by
error_get_last()
.
We must therefore make sure that the error actually occurred during the call
to fopen
:
@trigger_error('', E_USER_NOTICE); // reset
$file = fopen($path, 'r');
$error = error_get_last();
if ($error['message'] && the error['file'] === __FILE__ && $error['line'] === __LINE__ - 3) {
// an error occurred
}
The magic constant 3
is the number of lines between
__LINE__
and the call to fopen
. Please no
comments.
In this way, we can detect an error (if the function emits one, which the
aforementioned functions for working with regular expressions usually do not),
but we are unable to suppress it, i.e., prevent it from being logged, etc. Using
the shut-up operator @
is problematic because it conceals
everything, including any further PHP code that is called in connection with our
function (see the mentioned destructors, wrappers, etc.).
Custom Error Handler
The crazy but seemingly only possible way to detect if a certain function
threw an error with the possibility of suppressing it is by installing a custom
error handler using set_error_handler
. But it's no joke to
do it
right:
- we must also remove the custom handler
- we must remove it even if an exception is thrown
- we must capture only errors that occurred in the incriminated function
- and pass all others to the original handler
The result looks like this:
$prev = set_error_handler(function($severity, $message, $file, $line) use (& $prev) {
if ($file === __FILE__ && $line === __LINE__ + 9) { // magic constant
throw new Exception($message);
} elseif ($prev) { // call the previous user handler
return $prev(...func_get_args());
}
return false; // call the system handler
});
try {
$file = fopen($path, 'r'); // this is the function we care about
} finally {
restore_error_handler();
}
You already know what the magic constant 9
is.
So this is how we live in PHP.