Page 1 of 1

Make your PHP-Errors exceptional Exception handling in PHP Rate Topic: ***** 2 Votes

#1 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3524
  • View blog
  • Posts: 10,164
  • Joined: 08-June 10

Posted 09 June 2010 - 08:07 AM

*
POPULAR

Better Error Handling through Exceptions

note: you should have a basic understanding of Object Oriented Programming

when it comes to debugging, Iím always glad that PHP throws very descriptive errors, sometimes even providing solutions. While using procedural PHP code, you normally have no problem locating the part of your code which is the troublemaker. Alas, the picture becomes different, if you start coding PHP OOP style. You have objects calling each other, even loading themselves (thanks to the georgeous PHP SPL). Loosing the overview is just a matter of time.

Exceptions can be considered as advanced Errors, they donít just kill the script, but can be thrown and caught (i.e. you can repair the error) just as the program flows. A big advantage of Exceptions is, that they build a Stack (a list of every function in the current function stack, that the Exceptions travels through).

Example of Exception usage:
try 
{
	// try sending an email message via SMTP
	// the EMail class will throw an Exception, if there is for instance no connection
	$error_report = new EMail($emsg);
	$error_report->send();
}
catch (Exception $e)
{
	// if sending fails, at least log the error
	error_log($emsg);
}


Example of an Exception StackTrace:

Quote

#0 /****/main.errorhandler.php(54): ErrorLog::add(Array, '')
#1 [internal function]: KBL_main_ErrorHandler(2, 'PDO::__construc...', '***', 50, Array)
#2 /****/class.adb.php(50): PDO->__construct('mysql:host=loca...', '***', '***')
#3 /****/class.adb.php(83): aDB::connect()
#4 /****/class.articledb.php(70): aDB::prepare('ids', '????SELECT?????...')
#5 /****/class.articledb.php(93): ArticleDB->makeStatement('ids', '????SELECT?????...')
#6 /****/class.articledb.php(177): ArticleDB->load()
#7 /****/class.articlehtml.php(106): ArticleDB->requestHTML()
#8 /****/presse.php(10): ArticleHTML->display()
#9 /****/class.htmlplus.php(270): include('***')
#10 /****/main.php(52): HTMLplus->Inhalt()
#11 {main}

what you can see in this code is which functions/methods have been called until the Error/Exception was caught. Sometimes itís also important to know, which route the program was taking (since faulty parameters may be passed down for several levels before the cause an error).

but now back to topic. Exceptions are good and nice, but useless if you fail to extract the necessary information. In contrast to Errors, Exception do not cause a printed line on your page, so YOU have to take care of that.

The good news are: Exceptions are objects, now you can use your OOP skills to make them do what you want. Something you should do first is storing the Exceptions somewhere. Since Exceptions are caught everywhere, that has to be done in a globally accessible way. One possibility doing that uses the "Registry Pattern" (imagine a combination of the Singleton Pattern and an Array):
<?php
########################
# error storage object #
########################

/**
 * the information available from the thrown exceptions are stored here
 * for later processing (on destruct of main object)
 */
class ErrorEntry
{
	/**
	 * @var (string) $time         time the Exception is saved (= time thrown)
	 * @var (string) $method       method that catches the exception
	 * @var (int) $line            line that throws Exception
	 * @var (int) $code            description code
	 * @var (string) $level        severity level
	 * @var (string) $message      Exception message
	 * @var (string) $file         file that throws Exception
	 * @var (string) $trace        trace of the Exception
	 * @var (array) $mp_levels     transcription of the severity levels
	 * @var (array) $mp_labels     translateable labels for some of the 
	 *                             properties while printing
	 */
	protected $time;
	protected $method;
	protected $line;
	protected $code;
	protected $level;
	protected $message;
	protected $file;
	protected $trace;
	
	// if you want a translation
	public static $mp_levels = array('Error', 'Warning', 'Notice');
	public static $mp_labels = array('Function', 'File', 'Line');
	
	/**
	 * constructor method setting all properties. time is computed since 
	 * time differences shouldn't be much.
	 *
	 * @param (Exception) $exc     the caught Exception
	 * @param (string) $catch      method where Exception is caught (__METHOD__)
	 * @param (int) $level         severity of the Exception, is overridden
	 *                             by the Exception's severity value
	 * @return (void)
	 */
	function __construct($exc, $catch, $level = 0)
	{
		# Exception time
		$this->time    = date('r');
		# Exception caught in
		$this->method  = $catch;
		
		// from ErrorException
		if ($exc instanceof ErrorException)
		{
			$level     = $exc->getSeverity();
		}
		$this->level   = self::$mp_levels[$level];
		
		// from Exception
		$this->message = $exc->getMessage();
		$this->code    = $exc->getCode();
		$this->line    = $exc->getLine();
		$this->file    = $exc->getFile();
		$this->trace   = $exc->getTraceAsString();
	}
	
	/**
	 * read-only access to the properties.
	 */
	function __get($key)
	{
		$value = (isset($this->$key)) ? $this->$key : NULL;
		return $value;
	}
	
	/**
	 * print Exception trace as string representation.
	 */
	function __toString()
	{
		return $this->trace;
	}

	/**
	 * HTML formatted output of the entry.
	 *
	 * @param (bool) $f_trace      additionally printing backtrace?
	 * @return (string)            HTML version of Entry
	 */
	public function writeHTML($f_trace = true)
	{		
		$html = '
<h3>' . $this->level . ' [' . $this->code . ']</h3>
<p class="i temp">' . $this->time . '</p>
<p>' . $this->message . '</p>
<p>
	' . self::$mp_labels[0] . ' : ' . $this->method . '<br />
	' . self::$mp_labels[1] . ' : ' . $this->file . '<br />
	' . self::$mp_labels[2] . ' : ' . $this->line . '
</p>';
		
		if ($f_trace)
		{
			$html .= "\n<p>Backtrace:<br />" . $this->trace . '</p>';
		}
		
		return $html;
	}
	
	/**
	 * text output of the entry used for email (...)
	 *
	 * @return (string)            text version of Entry
	 */
	public function writeText()
	{
		$string = "\n" . $this->level . ' [' . $this->code . '] - ' . $this->time . '
' . $this->message . '
   ' . self::$mp_labels[0] . "\t : " . $this->method . '
   ' . self::$mp_labels[1] . "\t : " . $this->file . '
   ' . self::$mp_labels[2] . "\t : " . $this->line . '
' . $this->trace . "\n";
	
		return $string;
	}
}


<?php

### ATTENTION, this class uses custom code in the submit() method,
### which is not included here (but can be substituted)

abstract class ErrorLog
{
	/**
	 * @var (array) $Log        collection of ErrorEntry objects
	 */
	private static $Log = array();
	
	/**
	 * get the data collected in MyException() and create a new
	 * ErrorEntry object
	 * 
	 * @param (array) $bundle    an array as returned by the
	 *                           MyException::getBundle() method
	 * @param (string) $class    class name where the Exception
	 *                           was caught
	 * @return (void)
	 */
	public static function add(
		$input, 
		$catch,
		$level = 0
	) 
	{
		if ($input instanceof Exception)
		{
			self::$Log[] = new ErrorEntry($input, $catch, $level);
		}
		// at this point there is some more fetching code, 
		// which depends on a custom Exception class
		else
		{
			$emsg = "Incorrect parameter provided";
			throw new Exception($emsg);
		}
		return true;
	}
	
	/**
	 * standard get function of Abstract Registry Pattern
	 * 
	 * @param (string) $key      index name of log array
	 * @return (mixed)           NULL if key unknown
	 *                           ErrorEntry object
	 */
	public static function get(
		$key
	)
	{
		if (isset(self::$Log[$key])) 
		{
			return self::$Log[$key];
		}
		return NULL;
	}
		
	/**
	 * get the complete log (part of the Abstract Registry Pattern)
	 * 
	 * @return (array)          log array of ErrorEntry objects
	 */
	public static function getAll()
	{
		return self::$Log;
	}
	
	/**
	 * part of the Abstract Registry Pattern
	 * 
	 * @return (void)
	 */
	public static function reset()
	{
		self::$Log = array();
		return true;
	}

	public static function first()
	{
		if (0 == count(self::$Log)) 
		{
			return NULL;
		}
		reset(self::$Log);
		return current(self::$Log);
	}
	
	public static function next()
	{
		if (0 == count(self::$Log)) 
		{
			return NULL;
		}
		return next(self::$Log);
	}
	
	public static function prev()
	{
		if (0 == count(self::$Log)) 
		{
			return NULL;
		}
		return prev(self::$Log);
	}
	
	public static function last()
	{
		if (0 == count(self::$Log)) 
		{
			return NULL;
		}
		return end(self::$Log);
	}
	
	/**
	 * sending Error Report via Swift Mail implementation. if an error
	 * report is wanted in all applications add this method to the current
	 * objects destructor.
	 * 
	 * @return (bool)           submit success
	 */
	public static function submit()
	{
		if (empty(self::$Log)) return NULL;
		
		$text = 'Server "' . $_SERVER['HTTP_HOST'] . '":
';
		foreach (self::$Log as $entry)
		{
			$text .= $entry->writeText();
		}
	
		try 
		{
			// this is a custom email implementation, replace with your own
			$error_report = new EMail($text);
			$error_report->send();
		}
		catch (Exception $e)
		{
			echo '<p>', $e->getMessage(), '</p>';
			error_log($text);
			return false;
		}
		return true;
	}
}


now that we have the code for handling our Exceptions, we only need to call it properly.
to ease the loading, I recommend the SPLís spl_autoload_register() function. whatís left is catching the Exception:

// connect to MySQL using PDO
try 
{
	$dsn = 'mysql:host=' . DB_SERVER . ';dbname=' . DB_NAME;
	self::$PDO = new PDO($dsn, DB_USER, DB_PASS);
}
catch (PDOException $pdo)
{
	# log the Exception, anything else will be done by the system
	ErrorLog::add($pdo, __METHOD__);
	// do something about that
}

and finally send me the details

// part of the top level class
class Main
{
	// some hard working code

	public function __destruct()
	{
		ErrorLog::submit();
	}
}



whatís the benefit of that? Your users are not prompted with error messages (which btw. can be used to exploit your page), but you will be notified.

here is an example of the email body the above code produced

Quote

Server "localhost":

Error [12] - Mon, 25 Jan 2010 11:46:53 +0100
The ID "pod" is invalid.
Function : ID
File : /****/class.struktur.php
Line : 121
#0 /****/class.id.php(123): Struktur->checkID()
#1 /****/class.id.php(101): ID->validateID()
#2 /****/class.id.php(45): ID->buildSeite()
#3 /****/class.htmlplus.php(59): ID->__construct('main')
#4 /****/main.php(15): HTMLplus->__construct()
#5 {main}


I hope this small article will give you some new impulses when you code your next project.

Dormi

Is This A Good Question/Topic? 10
  • +

Replies To: Make your PHP-Errors exceptional

#2 szilard  Icon User is offline

  • New D.I.C Head

Reputation: 1
  • View blog
  • Posts: 17
  • Joined: 16-March 10

Posted 09 June 2010 - 11:42 AM

congratulation, this tutorial is very helpful for me
Was This Post Helpful? 1
  • +
  • -

#3 JBrace1990  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 110
  • View blog
  • Posts: 760
  • Joined: 09-March 08

Posted 09 June 2010 - 01:09 PM

Very good tutorial, though I prefer to output the errors to the browser and fix them there.
Was This Post Helpful? 0
  • +
  • -

#4 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3524
  • View blog
  • Posts: 10,164
  • Joined: 08-June 10

Posted 09 June 2010 - 01:14 PM

unfortunately, thatís not an option for a productivity server. nevertheless, just call the writeHTML() method.
Was This Post Helpful? 3
  • +
  • -

#5 Andy M  Icon User is offline

  • New D.I.C Head

Reputation: 3
  • View blog
  • Posts: 14
  • Joined: 09-June 10

Posted 09 June 2010 - 02:30 PM

I do agree this is a very useful tutorial that will fit into any kind of PHP scripting! Well done!
Was This Post Helpful? 0
  • +
  • -

#6 e_i_pi  Icon User is offline

  • = -1
  • member icon

Reputation: 795
  • View blog
  • Posts: 1,681
  • Joined: 30-January 09

Posted 11 May 2011 - 08:47 PM

Thanks again for your wonderful insight into PHP usage Dormi, always appreciated
Was This Post Helpful? 0
  • +
  • -

#7 e_i_pi  Icon User is offline

  • = -1
  • member icon

Reputation: 795
  • View blog
  • Posts: 1,681
  • Joined: 30-January 09

Posted 10 July 2011 - 11:30 PM

I've got a follow up question for discussion Dorm.

Supposing I also want to catch fatal errors (I'm still not sure that it is best to catch fatals, but for argument's sake, let's say I am). Will the register_shutdown_function() function need ErrorLog::submit() in it, or will the __destruct() method in Main fire despite the error being fatal?
Was This Post Helpful? 0
  • +
  • -

#8 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3524
  • View blog
  • Posts: 10,164
  • Joined: 08-June 10

Posted 11 July 2011 - 12:59 AM

as far as I know you canít catch fatal errors.

The PHP Manual said:

The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

Was This Post Helpful? 0
  • +
  • -

Page 1 of 1