Failing Gracefully with PHP 5

30 November -0001
Justin Klein Keane
April 22, 2007

Failing gracefully is often an application development goal that gets overlooked in pursuit of development. Quite often programmers are so task oriented that they focus on the goal of the software rather than the process. This is understandable since developers can predict input and flow in the controlled environments of their development setup. It isn't until software is released to actual users that unexpected inputs begin to crop up. Designing modular software that handles exceptions in a meaningful way is one of the touches that distinguishes good software from great software.

By writing code that handles errors and unexpected circumstances gracefully you can create more usable applications that will operate even under unusual circumstances. Let us examine a simple PHP application that connects to a database, queries persistent data found in the database and transforms it into an object for further use by the application. The data model is quite simple and consists of a single table 'test' in the database 'test' on localhost. The initial data looks like:

mysql> desc test;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| test_id   | int(11)      | NO   | PRI |         | auto_increment |
| test_data | varchar(255) | YES  |     |         |                |
+-----------+--------------+------+-----+---------+----------------+
2 rows in set (0.59 sec)

The PHP application code, at its simplest looks like this:

mysql_connect('localhost', 'user', 'password');
mysql_select_db('test',$conn);
mysql_query('select * from test',$conn);
while ($result = mysql_fetch_object($query)) {
	$retval[] = $result;
}

This series is typical of many PHP applications and has several problems. The first problem is that if the first line of the code fails the application doesn't detect this condition and the rest of the code executes, with each successive line also failing. The database connection fails, so the database selection fails, so the query fails, and so on. It would be much more efficient to allow the program to react to a database connection failure and handle that in a way that prevents further failures or execution of unnecessary instructions. The mysql_connection function returns a boolean value based on its success, and we can check this value to control our program flow. By updating the code so that it reads:

if (mysql_connect('localhost', 'user', 'password')) {
	mysql_select_db('test',$conn);
	mysql_query('select * from test',$conn);
	while ($result = mysql_fetch_object($query)) {
		$retval[] = $result;
	}
}
else {
	echo "Problem connecting to the database!";
}

With this revised code we have accomplished two goals. Firstly, we have provided a crafted error message that may be more useful to the user than the default PHP warning. The PHP server may even be configured to suppress warnings, in which case this error message would be the only visible message. Secondly, we have prevented unnecessary code execution, i.e. we have handled the error. Using this model we can control the program flow based on each step of the application, so that the code is proactive and avoids executing code that we can predict will fail and thus be extraneous. Continuing with this idea we can rewrite the code so that it looks like:

if ($conn = mysql_connect('localhost', 'user', 'password')) {
	if (is_resources($conn)) {
		mysql_select_db('test',$conn);
		$query = mysql_query('select * from test',$conn);
		if ($query) {
			while ($result = mysql_fetch_object($query)) {
				$retval[] = $result;
			}
			if (! is_array($retval)) {
				echo "Query returned no results.";
			}
		}
		else {
			echo "Query failed.";
		}
	}
}
else {
	echo "Problem connecting to the database!";
}

This new version of the code is much more tolerant of faults and is verbose in reporting errors. We can probably even make the code more compact by writing an error reporting function that we can call in a more streamlined fashion. Doing this we get:

$conn = mysql_connect('localhost', 'user', 'password')
		or err("Connection failed.",$conn);
mysql_select_db('test',$conn)
	or err("DB select problem.",$conn);
$query = mysql_query('select * from test',$conn)
	or err("Query failed.",$conn);
while ($result = mysql_fetch_object($query)) {
	$retval[] = $result;
}
if (isset($retval) && count($retval)<1) {
	err("Count returned no results.");
}

function err($msg,$res='') {
	if ($res != '' && is_resource($res)) {
		$msg .= mysql_error($res);
	}
	echo $msg;
}

Using this code we'll still have to check to make sure that the variable $retval was properly set before using it but you can begin to see how a viable class might be formed out of this code. By building a class we can further control the return values of the query, perhaps returning the boolean value false if something goes wrong. Let's go ahead and build a class out of this request so we can make the code more portable. Doing this we'll end up with the following code:

Class FindTest {

	public function loadTest() {
		$retval = false;
		if (! $conn = mysql_connect('localhost', 'user', 'pass')) {
			$this->err("Connection failed.",$conn);
			return false;
		}
		if (! mysql_select_db('test',$conn)) {
			$this->err("DB select problem.",$conn);
			return false;
		}
		$query = mysql_query('select * from test',$conn)
			or $this->err("Query failed.",$conn);
		if ($query) {
			while ($result = mysql_fetch_object($query)) {
				$retval[] = $result;
			}
			if (isset($retval) && count($retval)<1) {
				$this->err("Count returned no results.");
			}
		}
		return $retval;
	}

	private function err($msg,$res='') {
		if ($res != '' && is_resource($res)) {
			$msg .= mysql_error($res);
		}
		echo $msg;
	}
}
//application code
$test = new FindTest();
$retval = $test->loadTest();
if (! $retval) {
	echo "failed.";
}
else {
	echo "success.";
}

Now we've got an extremely robust interface to the rather simple operation. This interface will handle exceptional conditions and adjust program flow based on the results. The program also now presents useful error messages that help to indicate exactly what has gone wrong. The problem with these error messages is that they might not be useful to regular users. Additionally it might be useful to have these errors recorded in a persistent location for future reference. This would allow an admin to review errors and proactively respond to them (in the sense of responding to the error before it is reported by a user). Additionally we can suppress some messages so they don't display to the end user but are still available for the developer. While it would be nice to log these messages in the database, since we also want access to messages that indicate a data connection failure we'll need to store them somewhere else. Storing log messages on the filesystem is a good solution in this circumstance. We'll write new messages to a log using a Log class and then we can review error messages without having to rely on users recording and transmitting them to the developers. The following log class can be used:

Class Log {

	static private $instance = NULL;
	private $log_location = 'logs/messages.log';
	private $log_file;

	private function __construct() {
		if (! file_exists($this->log_location)) {
			touch($this->log_location)
				or die('Could not create log.');
		}
		$this->log_file = fopen($this->log_location, 'a')
			or die('Could not open error log.');
	}
	public function getInstance() {
		if (self::$instance == NULL)
			self::$instance = new Log();

		return self::$instance;

	}
	public function write_error($err) {
		$err = date('Y-m-d h:i:s') . "  " .
				$_SERVER['REMOTE_ADDR'] . "  " .
				$err . "\t" .
				"\n";
		if (! $this->log_file) $this->__construct();
		fwrite($this->log_file, $err)
			or die('Cannot write to error log.');
	}
}

Now we have an easy interface for the FindTest class to be able to write error messages to a persistent log file located in the logs directory. We can update the FindTest class to utilize this log without affecting the display application code at all. This new modification allows the application to log messages to a persistent log file without necessarily having to display any of the error messages to the user.

error_reporting(0);

Class Log {
	static private $instance = NULL;
	private $log_location = 'logs/messages.log';
	private function __construct() {
		if (! file_exists($this->log_location)) {
			touch($this->log_location)
			or die('Could not create the log.');
		}
		$this->error_log = fopen($this->log_location, 'a')
		or die('Could not open error log.');
	}

	public function getInstance() {
		if (self::$instance == NULL)
			self::$instance = new Log();

		return self::$instance;

	}

	public function write_message($msg) {
		$msg = date('Y-m-d h:i:s') . "  " .
				$_SERVER['REMOTE_ADDR'] . "  " .
				$msg . "\t" .
				"\n";
		if (! $this->error_log) $this->__construct();
		fwrite($this->error_log, $msg);
	}
}


Class FindTest {

	public function __construct() {
		$this->log = Log::getInstance();
	}

	public function loadTest() {
		$retval = false;
		if (! $conn = mysql_connect('localhost', 'user', 'pass')) {
			$this->err("Connection failed.",$conn);
			return false;
		}
		if (! mysql_select_db('test',$conn)) {
			$this->err("DB select problem.",$conn);
			return false;
		}
		$query = mysql_query('select * from test',$conn)
			or $this->err("Query failed.",$conn);
		if ($query) {
			while ($result = mysql_fetch_object($query)) {
				$retval[] = $result;
			}
			if (isset($retval) && count($retval)<1) {
				$this->err("Count returned no results.");
			}
		}
		return $retval;
	}

	private function err($msg,$res='') {
		if ($res != '' && is_resource($res)) {
			$msg .= mysql_error($res);
		}
		$this->log->write_message($msg);
	}
}

$test = new FindTest();
$retval = $test->loadTest();
if (! $retval) {
	echo "failed.";
}
else {
	echo "success.";
}

Notice that in this version of the code we've turned off error reporting with the PHP command error_reporting(). This allows us to suppress all the PHP messages and simply move messages to our log file system. You would probably also want some useful messages for the user display as well in this case to indicate an exceptional condition.

As you can easily see, the amount of code has increased dramatically. This increase in workload pays dividends however. Even though the code base is larger, it is much more robust. Additionally, the two classes we have created are portable, so we can use them in other places in the application, or even in other applications altogether with very little modification.

To make the application even more robust we could modify the logging so that it was configurable to different levels. We could have the log record only critical errors, warnings, or even debugging messages. Because the log is stored on the filesystem we can refer to the log file to follow up on user error reports to examine in more detail the underlying causes of potential errors. We can even write filesystem processes to monitor and operate on the log file, utilizing the power of the operating system to make the logs more useful.

Another potential approach that works quite nicely and is much less code intensive to set up is allowing PHP errors to write to a log file and suppressing their onscreen output. This method of handling errors isn't as granular as the above logging scheme. To set up this sort of a system you can modify the php.ini configuration file or simply utilize a .htaccess file if your system is configured to allow this. This .htaccess file could include the lines:

php_value display_errors false
php_value log_errors true
php_value error_log logs/php_err.log

In order to log custom messages to the error log you have to use the php command error_log() like so:

error_log("This is a custom error sent to the log");

In this way you can keep track of all of PHP's native warnings and error messages and add your own customized error messages to make the logs more informative.