Exceptions at the line
Exceptions are configured with the file and line where they were thrown. This is the classic situation : the exception is thrown immediately where a problem is detected.
<?php if (storeInDatabase($values) === NOT_STORED) { throw new StorageFailedException('Could not store in the database'); } ?>
Exceptions at a wrong line
It is not always convenient to throw the exception in the code. In particular, when a new layer of code is added to handle common operations. After all, this is what functions and classes are for.
<?php if (validateData($values)) { storeInDatabase($values); } function validateData($values) { if (validateValues($values) === MISSING_ID) { throw new \Exception('Id is missing id for '.$values->name); } if (validateValues($values) === MISSING_NAME) { throw new \Exception('Id is missing name for '.$values->id); } ... } ?>
Here, the validation is exported to a separate function, so as to avoid repeating (and forgetting) the various exceptions. One significant drawback of this situation is that validateData() now centralizes all the exceptions. Every detected exception is thrown from this particular function.
Fatal error: Uncaught Exception: Id is missing id for Smith in /tmp/test.php:9
Stack trace:
#0 /tmp/test.php(3): validateData(...)
#1 {main}
thrown in /tmp/test.php on line 9
A message to Exception
There is a solution to this problem : creating a custom exception. Exceptions, and all its native tree, are a good source of base exceptions. Most of the time, they are customized with a new message :
<?php class myException extends \Exception { function __construct($message) { parent::__construct($message); } } ?>
Yet, the exception classes include a lot of properties, including file and line number.
<?php class Exception extends Throwable { protected $message = 'Unknown exception'; // exception message private $string; // __toString cache protected $code = 0; // user defined exception code protected $file; // source filename of exception protected $line; // source line of exception private $trace; // backtrace private $previous; // previous exception if nested exception public function __construct($message = null, $code = 0, Exception $previous = null); ?>
This way, the exception may be designing another place in the code than the one where the exception was thrown :
<?php class myException extends \Exception { function __construct($message) { parent::__construct($message); $this->file = "myfile.php"; $this->line = 1000; } } ?>
The result looks like this :
Fatal error: Uncaught Exception: Id is missing id for Smith in myfile.php:1000
Stack trace:
#0 /tmp/test.php(3): validateData(...)
#1 {main}
thrown in /tmp/test.php on line 9
The right exception at the right place
You may notice that we only changed the file and line number with hard-coded values. This is not really helpful, since it doesn’t designate any real code. Using the magic constants __FILE__
and __LINE__
leads to the same problem : it designates the current code, not the calling code.
The calling code, and its predecessors, is available with a call to debug_backtrace(). This native function builds the stack of calls that lead to the current execution. It returns an array of arrays : each contains the file, line, function or method, class and type of call.
Here is the array, when debug_backtrace() is called with the DEBUG_BACKTRACE_IGNORE_ARGS
.
Array
(
[0] => Array
(
[file] => /tmp/test.php
[line] => 9
[function] => __construct
[class] => myException
[type] => ->
)
[1] => Array
(
[file] => /tmp/test.php
[line] => 3
[function] => validateData
)
)
We can now access the file and line we’d like to display :
<?php class myException extends \Exception { function __construct($message) { parent::__construct($message); $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $this->file = $trace[1]['file']; $this->line = $trace[1]['line']; } } ?>
The resulting default error message is now pointing to useful coordinates. It may be personalized further with __toString() or a getMessage() call, in the try/catch.
Fatal error: Uncaught Exception: Id is missing id for Smith in /tmp/test.php:3
Stack trace:
#0 /tmp/test.php(3): validateData(...)
#1 {main}
thrown in /tmp/test.php on line 9
Wrapping up
Exceptions may be configured beyond simple messages : it is possible to customise the file and line number, and the message displayed when the exception is used with echo. This is much more convenient than the default behavior.
Don’t forget to chain exceptions : always relay the previous exceptions to the newly created one, so that the whole chain of exceptions is available to the last receiver.