Structured Errors in PHP
We don't have them, yet, but we might in PHP 5.1. Here's a possible vision that will make both procedural and OO programmers happy. Before the vision, I'll summarize the main problems that need to be solved.
Problems
- Our error reporting mechanism consists only of severity level and textual error message. This makes it hard to handle specific errors from within our code.
- Accessing the last error from outside of an error handler is not possible without writing some glue code for yourself. You can turn on the track_errors setting to store the error message (not severity) in a local variable, but this doesn't buy you much.
- Since our regular mechanism is deficient, people want to use exceptions for everything. This is a problem, since exceptions are quite expensive and are not suitable to use for everything (such as E_NOTICE severity) for all extensions. It might make sense to do this for a particular extension in a particular piece of code in a particular application, but not globally. In addition, for people using code libraries, the library needs things to work one way, whereas the application wants things to work in another.
Solution
- Introduce error code identifiers. These identifiers will be strings prefixed with the name of the extension and a colon. So, errors from the standard extension would have identifiers such as "standard:<errorcode>". These identifiers can be examined in the PHP code more easily than parsing error message text, and since they are prefixed by the extension name, it side-steps the event where two different libraries use the same error number for different error conditions--we won't suffer from those collisions.
- The error identifiers would be raised along with the severity and textual message when the extension calls one of the php_error_docref style functions.
- The error handling mechanism would populate an $_ERROR super-global with the severity, identifier and textual message. This allows the PHP script to suppress the usual error handler and make decisions based on the info it finds in $_ERROR, if they wish.
For OOP Programmers
- Since all errors/warnings/notices are now structured, it would be really easy to map them to exceptions. This mapping needs to controlled within the script using a kind of stacking state. When the engine starts running, the mapping state is set so that no errors are mapped to exceptions.
- An application or library could then change this so that errors from a particular extension are mapped to exceptions, or so that all errors are mapped to exceptions by using a simple pattern matching rule. This state needs to be applied to a block of code, so that setting is contained and doesn't mess with that of the calling code. The declare statement is ideal for this.
Sample for procedural programmers
<?php function do_something_with_a_file($filename) { // ensure that streams functions aren't mapped to exceptions // everything else retains its current exception mapping declare(exception_map='-standard:streams:*') { $fp = @fopen($filename, 'r'); if (!$fp) { if ($_ERROR['code'] == 'standard:streams:E_NOENT') { // handle the case where the file doesn't exist } } } // now the declare block is finished, pop back to original // exception mapping state } ?>
Sample for OO prgrammers
<?php function do_something_with_a_file($filename) { // ensure that streams functions are mapped to exceptions // everything else retains its current exception mapping declare(exception_map='+standard:streams:*') { try { $fp = @fopen($filename, 'r'); } catch (Exception $e) { if ($e->getCode() == 'standard:streams:E_NOENT') { // handle the case where the file doesn't exist } } } // now the declare block is finished, pop back to original // exception mapping state } ?>
As I hope you can see, this allows some flexibility in your code. You can code OO-style if you like. You can mix code snippets written using conflicting a style into your application, since well written libraries will localize their error handling preferences.
The exception mapping syntax used in the declare block should be quite simple to grasp; a plus or a minus character indicates if the pattern should be added to mapping list, or excluded from it. The rest of the string is a simple glob-style string where an asterisk acts as a wildcard. To make multiple changes, without using multiple nested declare blocks, simply separate each one by commas in the string:
<?php // don't map any errors from ext/standard, except // for streams errors declare(exception_map='-standard:*,+standard:streams:*') { ... } ?>
I'm fairly happy with this idea; the only thing is that the syntax in the declare block is a bit weird; it might make sense to come up with an alternative language level keyword. The important point is that any changes made to the mapping stack are popped when control leaves that section of code. If we had the finally statement, then we could do something like this:
<?php function do_something_with_a_file($filename) { // ensure that streams functions aren't mapped to exceptions // everything else retains its current exception mapping push_exception_map('-standard:streams:*'); try { $fp = @fopen($filename, 'r'); if (!$fp) { if ($_ERROR['code'] == 'standard:streams:E_NOENT') { // handle the case where the file doesn't exist } } } finally { pop_exception_map(); } } ?>
I like this better; it's less stuff to hack into the engine.
In conclusion then, I think this possible solution will please pretty much all the people using PHP, whether they are fans of OO or procedural code--you can write your "Enterprise" level code regardless of your preference, and drop-in well written third-party components without worrying so much about how they handle errors.
I welcome your comments!