While searching for ready exception handler I did not found any that could match following requirements:
1) Handle Exception and any other (AppException, MyAppException etc) exception that are Exception super class;
2) Send email notification about exception;
3) Dump exception to log;
4) Show nice page to the user:
a) A custom landing page can be defined for exception;
b) For all other exceptions a default landing page should be used;
5) Landing page text should be localizable;
As a good start I’ve used existing AppException implementation [1]. It matches requirements: 4 and 5, but can not handle Exceptions, only AppException. So, here my evolved version of the code:

<?php
/**
  * Aplication wide exception handler class.
  *
  * Installation: 
  * 1) Copy file in to the app directory;
  * 2) Add to bootstrap.ctp folowing lines:
  * require_once(APP.'app_exception.php');
  * set_exception_handler(array('AppExceptionHandler', 'handleException'));
  * 3) Add default view to render exceptions in /exceptions/unknown.ctp, 
  * view has access to $info variable
  *
  * References:
  * http://www.mt-soft.com.ar/2007/12/21/handling-exceptions-in-cakephp-12/
  */
class AppExceptionHandler extends Object {  
 
	static function handleException($exception) {
		$parsed = new AppExceptionParser($exception);
		$instance = new AppExceptionHandler();
		$instance->renderException($parsed->getInfo());
		$instance->logException($parsed->getInfo());
		$instance->emailException($parsed);
		exit;
	}
 
	function renderException($info = array()) {
		$Controller = new Controller();
		$Controller->viewPath = 'exceptions';
		$Controller->layout = 'exception';
		$Dispatcher = new Dispatcher();
		$Controller->base = $Dispatcher->baseUrl();
		$Controller->webroot = $Dispatcher->webroot;
		$Controller->set(compact('info'));
		$View = new View($Controller);
		$view = @$info['type'];
		if (!file_exists(VIEWS.'exceptions'.DS.$view.'.ctp')) {
			$view = 'unknown';
		}
		//header('HTTP/1.0 500 Internal Server Error');
		$out = $View->render($view);
		$Controller->afterFilter();  
		echo $out;
	}
 
	function logException($info) {
		$this->log(serialize($info), LOG_ERROR);
	}
 
	function emailException($message) {
		App::import('Core', 'Email');  
		$email = new EmailComponent;  
		$email->from = 'noreply@example.com';  
		$email->to = 'dev@example.com';  
		$email->sendAs = 'text';  
		$email->subject = 'Exception notification';  
		$email->send($message);  
	}
}
 
/**
  * Application exception info synthetic class.
  */
class AppExceptionParser extends Object {
 
	var $exception = null;
 
	function __construct($exception) {
		$this->exception = $exception;
	}
 
    public function __toString() {
		return print_r($this->getInfo(), true);
    }	
 
	function getInfo() {
		return array_merge(
			array(
				'type' => $this->getType(),
				'message' => $this->getMessage()
			),
			$this->where(), 
			$this->context()
		);
	}
 
	function getType() {
		if (get_class($this->exception) == 'AppException' || is_subclass_of($this->exception, 'AppException')) {
			return $this->exception->getType();
		}
		else return 'unknown';
	}
 
	function getMessage() {
		return $this->exception->getMessage();
	}
 
	function where() {
		return array(
			'function' => $this->getClass().'::'.$this->getFunction(),
			'file' => $this->exception->getFile(),
			'line' => $this->exception->getLine(),
			'url' => $this->getUrl()
			);
	}
 
	function getUrl($full = true) {
		return Router::url(array('full_base' => $full));
	}
 
	function getClass() {
		$trace = $this->exception->getTrace();
		return $trace[0]['class'];
	}
 
	function getFunction() {
		$trace = $this->exception->getTrace();
		return $trace[0]['function'];
	}
 
	function context() {
		return array(
			'remoteAddr' => $this->getRemoteAddr(),
			'requestMethod' => $this->getRequestMethod(),
			'httpUserAgent' => $this->getHttpUserAgent(),
			'httpAcceptLangage' => $this->getHttpAcceptLanguage(),
			'httpReferer' => $this->getHttpReferer(),
			'sessionAuth' => $this->getSessionAuth()
			);
	}
 
	function getRemoteAddr() {
		return $_SERVER['REMOTE_ADDR'];
	}
 
	function getRequestMethod() {
		return $_SERVER['REQUEST_METHOD'];
	}
 
	function getHttpUserAgent() {
		return $_SERVER['HTTP_USER_AGENT'];
	}
 
	function getHttpAcceptLanguage() {
		return $_SERVER['HTTP_ACCEPT_LANGUAGE'];
	}
 
	function getHttpReferer() {
		return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : 'None';
	}
 
	function getSessionAuth() {
		return isset($_SESSION['Auth']) ? print_r($_SESSION['Auth'], true) : 'Anonymous';
	}
 
}
 
/**
  * Application base exception class. The $info['type'] stores name of the view file in /views/exceptions/, without .ctp.
  * Default view is /views/exceptions/unknon.ctp
  */
class AppException extends Exception {
 
	var $type = null;
 
	function __construct($message, $type = 'unknown') {
		parent::__construct($message);
		$this->type = $type;
	}
 
	function getType() {
		return $this->type;
	}
 
}
?>

To attach AppExceptionHandler to yours the CakePHP application add to the bootstrap.php following lines:

require_once(APP.'app_exception.php');
if (configure::read() <= 1) {
	set_exception_handler(array('AppExceptionHandler', 'handleException'));
}

The last step is to create /views/exceptions/unknown.ctp, here is my example:

<div>
	<p><img src="/img/logo.png"/></p>
	<h1><?php __('-views-exceptions-unknown-header') ?></h1>
	<p><?php __('-views-exceptions-unknown-text-short_term'); ?></p>
	<p><?php __('-views-exceptions-unknown-text-long_term'); ?></p>
 
	<div id="debug" class="cake-exception-log">
	<table><?php 
		if (Configure::read() >= 1) {
			foreach ($info as $name => $value) {
				echo '<tr><th>'.$name.':</th><td id="'.$name.'">'.$value.'</td></tr>';
			}
		}
	?></table>
	</div>
</div>

That’s it. Hope this helps.

References:
[1] http://www.mt-soft.com.ar/2007/12/21/handling-exceptions-in-cakephp-12/

Posted by Rostislav Palivoda, filed under PHP. Date: March 30, 2009, 9:34 pm |

5 Responses

  1. José Pedro Saraiva Says:

    Excellent!

    This was just what I was looking for! Great job!

    Thank you,
    Best regards.

  2. redthor Says:

    Hi, thanks to you and debuggable for this.

    I’m experiencing something strange and wondered if you had any tips…?

    I sometimes will get an exception from a zend component. I catch that exception and raise my own AppException. Further up the chain I check if I get an AppException of a certain type, and if I do I will handle it nicely.

    The thing is that I’d still like to log the original zend component error. But if I attempt to use AppExceptionHandler::handleException() it will stop execution of the code.

    Here is an example:

    try {
    // This will sometimes throw an error if the connection fails
    $newConn = new Zend_Connection($connection);
    } catch (Exception $e) {
    // Log/email/print the details of the error
    AppExceptionHandler::handleException($e);

    // WE NEVER GET HERE
    $this->log(’CATCH THIS?’, LOG_WARNING);

    throw new AppException(’We have a problem’, ‘connect_failed’);
    }

    If I comment out AppExceptionHandler::handleException($e); I will get the log and my AppException being thrown. Otherwise the code exits at handleException(). I can’t see anywhere that your code ‘throws’ an exception…

    Anyway, if you have any ideas, let me know. Thanks!

  3. redthor Says:

    oh, how silly. I was staring right at it!
    There’s an exit at the end of handleException() method…
    Do you think that’s required?

  4. Babar Says:

    It is not handling exceptions like if i provide invalid database or invalid host or may be i m missing some thing :(

  5. Rostislav Palivoda Says:

    It’s not handling this exceptions because they are handled by CakePHP :)

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.