Abusing Exceptions to provide model-layer redirection
Every so often, I wish to both raise an exception to the UI as well as redirect to a specific page on the site. Normally, I’ll have code that looks like the following:
<?php
class PostsController extends AppController {
public function view($id) {
try {
$post = $this->Post->findById($id);
catch (MissingPost $e) {
$this->Session->setFlash("No post available");
return $this->redirect('posts/index');
} catch (NoPostPermissions $e) {
$this->Session->setFlash("You can't view this");
return $this->redirect('users/home');
} catch (UnapprovedPost $e) {
return $this->redirect('posts/index');
} catch (Exception $e) {
$this->Session->setFlash($e->getMessage());
return $this->redirect('posts/index');
}
$this->set(compact('post'));
}
}
?>
The biggest issues here are setting session flash messages and handling the redirects. To handle custom redirects from exceptions, we’ll likely want to create a custom exception class where we can attach routing data:
<?php
// app /Lib/Exception/AppException.php
class AppException extends CakeException {
public function setRoute($route = null) {
$this->_attributes['route'] = $route;
}
public function getRoute() {
return $this->_attributes['route'];
}
public function hasRoute() {
return isset($this->_attributes['route']);
}
}
?>
Whenever we wish to use this exception, we simply do the following:
<?php
App::uses('AppException', 'Lib/Exception');
$exception = new AppException("Some error occurred");
$exception->setRoute('account/index');
throw $exception;
?>
Note: This isn’t very fluent api. A possible solution would be to do something like:
<?php
// app /Lib/Exception/AppException.php
class AppException extends CakeException {
public function __construct($message, $code = 0) {
parent::__construct($message, $code);
return $this;
}
public function setRoute($route = null) {
$this->_attributes['route'] = $route;
return $this;
}
public function getRoute() {
return $this->_attributes['route'];
}
public function hasRoute() {
return isset($this->_attributes['route']);
}
}
?>
and then simply do throw new AppException("Some error message")->setRoute('account/index');
.
Next, you’ll want to create a custom ErrorHandler
. CakePHP allows you to override the built-in one, and while we want most of the original internals, we’ll override for our very own exception:
<?php
App::uses('AppException', 'Lib/Exception');
App::uses('ErrorHandler', 'Error');
class AppErrorHandler extends ErrorHandler {
public static function handleException(Exception $exception) {
if ($exception instanceof AppException) {
$element = 'default';
$message = $exception->getMessage();
$params = array('class' => 'error');
CakeSession::write('Message.flash', compact('message', 'element', 'params'));
if ($exception->hasRoute()) {
$controller = self::_getController($exception);
return $controller->redirect($exception->getRoute());
}
}
return parent::handleException($exception);
}
/**
* Get the controller instance to handle the exception.
* Override this method in subclasses to customize the controller used.
* This method returns the built in `CakeErrorController` normally, or if an error is repeated
* a bare controller will be used.
*
* @param Exception $exception The exception to get a controller for.
* @return Controller
*/
protected static function _getController($exception) {
App::uses('CakeErrorController', 'Controller');
if (!$request = Router::getRequest(true)) {
$request = new CakeRequest();
}
$response = new CakeResponse(array('charset' => Configure::read('App.encoding')));
try {
if (class_exists('AppController')) {
$controller = new CakeErrorController($request, $response);
}
} catch (Exception $e) {
}
if (empty($controller)) {
$controller = new Controller($request, $response);
$controller->viewPath = 'Errors';
}
return $controller;
}
}
?>
Now that this is available, lets configure it in our core.php
:
<?php
App::uses('AppErrorHandler', 'Lib/Exception');
App::uses('AppException', 'Lib/Exception');
Configure::write('Exception', array(
'handler' => 'AppErrorHandler::handleException',
'renderer' => 'ExceptionRenderer',
'log' => true
));
?>
And presto! Now you can throw exceptions with custom routes associated! Now you can start doing the following:
<?php
class NoPostPermissions extends AppException {
public function __construct($message, $code = 0) {
$message = "You can't view this";
parent::__construct($message, $code);
$this->setRoute('users/home');
return $this;
}
}
?>
And our above example controller with weird exception handling now becomes:
<?php
class PostsController extends AppController {
public function view($id) {
$post = $this->Post->findById($id);
$this->set(compact('post'));
}
}
?>
Simplify your exception handling logic in controllers today!