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