debuggable

 
Contact Us
 

Exceptional Cake

Posted on 21/10/07 by Felix Geisendörfer

Hey folks,

sorry for letting you guys wait so long, but here is my promised post on how to use Exceptions in CakePHP. Before you continue reading, be warned that you'll need PHP 5 as well as CakePHP 1.2 for this code to work properly.

First of all. Why did I decide to experiment with exceptions in CakePHP? Well, Object::cakeError() does an ok job at providing me with a way to render some sort of internal error while I'm debug mode. However, I think that is what its really meant for, and its not the way to go for rendering errors to the user directly. Besides you cannot really use it within a static function call, a class that is not a descendant of 'Object', nor do you have any way of "catching" an error thrown this way. All of these things can be addressed by using PHP5s support for custom Exception classes quite elegantly.

But lets look at the code before I explain even further. Put this in /app/error.php:


uses('error');
/**
 * undocumented class
 *
 * @package default
 * @access public
 */

class AppError extends ErrorHandler{
/**
 * New Exception handler, renders an error view, then quits the application.
 *
 * @param object $Exception AppException object to handle
 * @return void
 * @access public
 */

   static function handleException($Exception) {
      $Exception->render();
      exit;
   }
/**
 * Throws an AppExcpetion if there is no db connection present
 *
 * @return void
 * @access public
 */

   function missingConnection() {
      throw new AppException('db_connect');
   }
}
set_exception_handler(array('AppError', 'handleException'));

/**
 * undocumented class
 *
 * @package default
 * @access public
 */

class AppException extends Exception {
/**
 * Details about what caused this Exception
 *
 * @var array
 * @access public
 */

   var $info = null;
/**
 * undocumented function
 *
 * @param mixed $info A string desribing the type of this exception, or an array with information
 * @return void
 * @access public
 */

   function __construct($info = 'unknown') {
      if (!is_array($info)) {
         $info = array('type' => $info);
      }
      $this->info = $info;
   }
/**
 * Renders a view with information about what caused this Exception. $info['type'] is used to determine what
 * view inside of views/exceptions/ is used. The default is 'unknown.ctp'.
 *
 * @return void
 * @access public
 */

   function render() {
      $info = am($this->where(), $this->info);
     
      $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");
      return $View->render($view);
   }
/**
 * Returns an array describing where this Exception occured
 *
 * @return array
 * @access public
 */

   function where() {
      return array(
         'function' => $this->getClass().'::'.$this->getFunction()
         , 'file' => $this->getFile()
         , 'line' => $this->getLine()
         , 'url' => $this->getUrl()
      );
   }
/**
 * Returns the url where this Exception occured
 *
 * @return string
 * @access public
 */

   function getUrl($full = true) {
      return Router::url(array('full_base' => $full));
   }
/**
 * Returns the class where this Exception occured
 *
 * @return void
 * @access public
 */

   function getClass() {
      $trace = $this->getTrace();
      return $trace[0]['class'];
   }
/**
 * Returns the function where this Exception occured
 *
 * @return void
 * @access public
 */

   function getFunction() {
      $trace = $this->getTrace();
      return $trace[0]['function'];
   }
}

You'll also need this in your /app/config/bootstrap.php file:

require_once(APP.'error.php');

Now you can do cool stuff like this:

function view($id = null) {
   $this->Task->set('id', $id);
   if (!$this->Task->exists()) {
      throw new AppException(array('type' => '404', 'id' => $id));
   }
   // ...
}

Or like this:

static function svnVersion() {
   static $version = null;
   if (!is_null($version)) {
      return $version;
   }
   
   $version = trim(shell_exec("svn info ".ROOT." | grep 'Changed Rev' | cut -c 19-"));
   if (empty($version)) {
      throw new AppException('no_working_copy');
   } elseif (!is_int($version) || !($version > 0)) {
      throw new AppException('svn_version');
   }
   return $version;
}

Or just as simple as:

function utcTime() {
   $time = strtotime(gmdate('Y-m-d H:i:s'));
   if (!is_numeric($time)) {
      throw new AppException();
   }
   return $time;
}

In either case you'll need a new 'exception.ctp' layout. This layout should be very simple, and ideally work even if no Models could have been loaded or other parts of your system have failed. If you have a dynamic navigation, this means either falling back to a default one, or not displaying anything but a back button.

After you created that you also need a default exception view called 'unknown.ctp'. Mine looks simply like this:

<h1><?php echo $this->pageTitle = 'Oops, an internal error occured'; ?></h1>
<p>Sorry, but something must have gone horribly wrong in the internal workings of this application.</p>

For exceptions that are associated with a HTTP response status like '404', I recommend a view like this:

<h1><?php echo $this->pageTitle = '404 - Page not found'; ?></h1>
<p>We are sorry, but we could not locate the page you requested on our server.</p>

Alright this is nice ... but you can do even more! Having the unified AppError::handleException function allows you to do fun things like logging your exceptions, or even sending out notification emails to the system administrator. Oh and its also very convenient if you want to catch only certain kinds of Exceptions:

try{
   $version = Common::svnVersion();
} catch (AppException $Exception) {
   if ($Exception->info['type'] != 'no_working_copy') {
      AppError::handleException($Exception);
   }
   $version = 'HEAD';
}

One of the things I'm currently trying to do with Exceptions is to build my current application so that it fails fast. By that I mean that I rather have the user see a big fat error message instead of trying to recover from failing functions. Jeff Atwood has an interesting article on this subject which I mostly agree with. However with web applications I feel like we can justify seeing our users see our application crashing hard much more often then with desktop software. That is because its simply much easier to fix the problem for everybody - no software update needed. If you go this path however, please make sure you have rock-solid error reporting form or an e-mail address independent from the server where the app runs on, and mention those in your exception layout.

Anyway, I'm going to periodically make changes to this AppException class and eventually add support to allow people to customize its behavior (like add logging) without having to change the code itself. For now however this should give you some inspiration on how you could leverage some of that yummy PHP5 goodies that a lot of us cake folks sometimes forget about (just b/c cake is PHP4 compatible it doesn't mean our apps have to be!).

Hope some of you find this useful,
-- Felix Geisendörfer aka the_undefined

 
&nsbp;

You can skip to the end and add a comment.

JadB said on Oct 21, 2007:

Thanks for sharing!

I haven't worked with exceptions yet, but that definitely gives a good start. I also approached the error handling in cake but using a much simpler way - you really got me thinking now. Haven't really implemented my solution across the app yet, so I will try this one and see which one I am comfortable with best. Thanks again.

PHPDeveloper.org said on Oct 22, 2007:

Felix Geisendorfer's Blog: Exceptional Cake...

...

[...] Felix Geisendorfer has posted a new blog entry he’s been promising for a while now - a look at using Exceptions in a CakePHP application: First of all. Why did I decide to experiment with exceptions in CakePHP? Well, Object::cakeError() does an ok job at providing me with a way to render some sort of internal error while I’m debug mode. However, I think that is what its really meant for, and its not the way to go for rendering errors to the user directly. […] [All of] these things can be addressed by using PHP5s support for custom Exception classes quite elegantly. [...]

[...] if you liked my last post on how to use PHP5 Exceptions in CakePHP, then here is a little addition to it: [...]

Yevgeny said on Oct 24, 2007:

I play with you solution and got one problem.
Some vendor application use exceptions for interract. In this case we should not handle Exceptions that does not instance of AppException.

So need to change AppError::handleException to process only such Exception that is instance of AppException.

But how to rethrow unknow types exceptions i dont know (different application use many type of Exception class child).

Felix Geisendörfer said on Oct 24, 2007:

Yevgeny: Good point, I'll try to add this. But I'm not sure how to re-throw non AppExceptions either. Anybody?

nao  said on Oct 25, 2007:

@Yevgeny and @Felix

"You can even rethrow exceptions from nested try/catch-blocks, in case you'd like to pass an Exception on to a place you have more control over its handling."

http://www.andreashalter.ch/phpug/20040115/3.html

nao  said on Oct 25, 2007:

you can do something like that (not tested) :http://bin.cakephp.org/view/428473160

nao  said on Oct 25, 2007:

Last link don't work : try this : http://bin.cakephp.org/view/1799720146

Yevgeny said on Oct 26, 2007:

@NAO:
I start from such tests. You right rethrow work well inside catch but php create new Exception object in we call throw from rethrowException function. In this case we lost all info about exception.

Scott Martin  said on Jul 08, 2008:

I think you need and echo or print statement on line 18 of your error.php file. It took me a while to figure out why nothing was being sent to the browser.

Tim Koschützki said on Jul 18, 2008:

There have been made a few changes to this. For example one would need to call AppController::beforeRender() to push any stuff to the view that would also display for the exception layout. Think of an exception layout that still makes use of a left column or so.

We might re-release this sometime.

Matt Hamann said on Nov 21, 2008:

This is pretty cool, but when I try using it, I get a fatal error within app_controller.php saying 'Class Controller could not be found.' Any idea what's going on?

Tim Koschützki said on Nov 21, 2008:

Yeah, Matt. Add this before your class AppController { .. } :

App::import('Core', 'Controller');

Matt Hamann said on Nov 21, 2008:

Nice! It's working great!

TEHEK said on Jun 29, 2009:

Hey, nice job there! Hope in future they will use something like this to do something decent with the error output to users.

I'm running CakePHP 1.2.3 and I had to do some change to your code:

app/error.php : Line 18

Replace
$Exception->render();

with
print $Exception->render();

// actually, how did it work without it in the first place? O_o

also thinking of adding layout switcher in case something except standard text input will be needed..

THanks again!

Kjell said on Jul 25, 2009:

hehehe. I should have found this earlier! Coded just about the same thing a few hours ago.

Anyway.. This is probably a good idea, too:

if (Configure::read('debug')) {
$Controller->viewPath.= DS . 'dev';

} else {

$Controller->viewPath.= DS . 'prod';

}

So you have two subfolders in "views/exceptions". One holds templates for the developers with trace and whatnot, and the other one contains templates describing the issue for mere mortals.

The file_exists check should come after that and use the Controller->viewPath as path variable instead.

Kjell

PS: support code/pre in comments, plz :)

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.