Na navigaci | Klávesové zkratky

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.


phpFashion © 2004, 2024 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.