Convenient controller’s callback methods with CakePHP
Controller’s callback methods (beforeFilter, beforeRender and afterFilter) are very useful, they save a lot of time, makes code more readable and DRY. You probably use them a lot. They are great. Unless they’re too big.
The process is simple.
- You want some code executed before every action in your controller, so you put the code in beforeFilter callback.
- Then you want some code executed only before certain actions, so you add the if-then-else block to your before filter that deciced whether to execute the code or not.
- Some code needs to be executed only if the user is logged in, so another if-then-else block is created.
- More conditions creates more mess in your callback…
Could it be changed? Well, look at following snippet which comes from Ruby on Rails.
class UsersController < ApplicationController before_filter :do_something, :do_always before_filter :do_something_else, :only => [:show, :new] before_filter :do_something_different, :except => :edit # method definitions commented out end
What will happen:
- do_something and do_always methods will be called during every request
- do_something_else method will be called only if show or new actions are requested
- do_something_different method will be called during every request except those to edit action
Clean and simple. Every piece of code that needs to be called in before_filter callback (under whatever conditions) is placed in it’s own method. The controller decides what methods to invoke and when.
I prefer this approach to Cake’s built-in one, so I decided to port it.
You need to put this code into your AppController.
class AppController extends Controller { function _callbacks($callbacks) { $defaults = array( 'methods' => array(), 'only' => array(), 'except' => array(), 'if' => array(), 'unless' => array() ); $ifs = $unlesses = $methods = array(); foreach ($callbacks as $array) { $array = am($defaults, $array); foreach ($array as $key => $value) { if (!is_array($value)) { $array[$key] = array($value); } } $ok = true; foreach ($array['if'] as $if) { if (!array_key_exists($if, $ifs)) { $ifs[$if] = $this->dispatchMethod("_$if"); } $ok = $ok && $ifs[$if]; if (!$ok) { break; } } foreach ($array['unless'] as $unless) { if (!array_key_exists($unless, $unlesses)) { $unlesses[$unless] = $this->dispatchMethod("_$unless"); } $ok = $ok && !$unlesses[$unless]; if (!$ok) { break; } } if ($ok) { if (!empty($array['only'])) { if (!in_array($this->action, $array['only'])) { $ok = false; } } elseif (!empty($array['except'])) { if (in_array($this->action, $array['except'])) { $ok = false; } } } if ($ok) { $methods = am($methods, $array['methods']); } } foreach (array_unique($methods) as $method) { $this->dispatchMethod("_$method"); } } function __mergeVars() { $pluginName = Inflector::camelize($this->plugin); $pluginController = $pluginName . 'AppController'; if (is_subclass_of($this, 'AppController') || is_subclass_of($this, $pluginController)) { $appVars = get_class_vars('AppController'); $uses = $appVars['uses']; $merge = array('beforeFilter', 'afterFilter', 'beforeRender'); $plugin = null; if (!empty($this->plugin)) { $plugin = $pluginName . '.'; if (!is_subclass_of($this, $pluginController)) { $pluginController = null; } } else { $pluginController = null; } if ($pluginController) { $pluginVars = get_class_vars($pluginController); } foreach ($merge as $var) { $appVar = (isset($appVars[$var]) && !empty($appVars[$var]) && is_array($appVars[$var])) ? $appVars[$var] : array(); $pluginVar = (isset($pluginVars[$var]) && !empty($pluginVars[$var]) && is_array($pluginVars[$var])) ? $pluginVars[$var] : array(); $thisVar = (isset($this->{$var}) && !empty($this->{$var}) && is_array($this->{$var})) ? $this->{$var} : array(); $this->{$var} = am($appVar, $pluginVar, $thisVar); } } parent::__mergeVars(); } function beforeFilter() { if (!empty($this->beforeFilter)) { $this->_callbacks($this->beforeFilter); } } function afterFilter() { if (!empty($this->afterFilter)) { $this->_callbacks($this->afterFilter); } } function beforeRender() { if (!empty($this->beforeRender)) { $this->_callbacks($this->beforeRender); } } }
Pretty long but worth it. Now you can define in controllers (or AppController, or PluginAppController):
var $beforeFilter = array( array( 'methods' => array('do_something', 'do_always') ), array( 'methods' => array('do_something_else'), 'only' => array('show', 'new') ), array( 'methods' => array('do_something_different'), 'except' => array('edit') ) ); function _do_something() { } function _do_always() { } function _do_something_else() { } function _do_something_different { }
This is equivalent to Rails definition above. Note the underscores before function names. They help distinguish those methods from usual controller actions.
You can do even more:
var $beforeFilter = array( array( 'methods' => array('do_something'), 'if' => array('is_admin') ), array( 'methods' => array('do_something_else'), 'unless' => array('is_logged_in') ) ); function _logged_in() { } function _is_admin() { }
Now do_something method will be called if _is_admin returns true, and do_something_else if _is_logged_in returns false. It’s self-explanatory I think.
Of course you can use all options (only, except, if, unless) together or mix them.
The definitions are processed in order they appear in $beforeFilter (or $beforeRender, or $afterFilter) array.
The definitions from your controller are merged with those defined in PluginAppController and AppController (thanks to redefined __mergeVars() method which is called during controller construction). Firstly AppController’s definitions are processed, then PluginAppController’s, and your controller’s at last.
Enjoy and comment please.
Related posts
6 Responses to 'Convenient controller’s callback methods with CakePHP'
Sorry, comments are closed for this post.

This is great…many thanks for sharing!
Lucian Lature
21 Aug 08 at 08:07
[...] Convenient controller’s callback methods with CakePHP Adds a Rails like "only" and "except" filter to beforeFilter. (tags: cakephp beforeFilter) [...]
links for 2008-08-22 « Richard@Home
22 Aug 08 at 10:02
[...] time ago I wrote how to improve Cake controllers’ callback system basing on a solution from Ruby on Rails. Today I am going to port another cool feature of Rails to [...]
Named scope in CakePHP » code tunes
5 Sep 08 at 14:35
Hey man, you got some great ideas here. If I ever need this I will remember your post.
Jonah Turnquist
15 Oct 08 at 21:31
Hi,
I am newbie to cakephp.But I know ruby-rails some extent.
Your Idea is looking great.But it’s not working for me.I don’t know where I’m doing wrong.
I just copied the code into /libs/controller/app_controller.php.
I added the following code to my controller ,
var $beforeFilter = array(
array(
‘methods’ => array(’check_session’),
‘except’ => array(’login’)
)
);
function _check_session() { echo ‘…..checking session….’;}
I can’t see the output on browser “…..checking session….”.
How _callbacks() gets called? I had put some echo statements in the function. But I can’t see any of those on the browser.
Hope any one would help me.
Archana
Archana
17 Mar 09 at 15:30
You need to define regular beforeFilter, beforeRender (etc) methods in your AppController that call _callbacks(method). Check the code above again.
There is also possibility that you have regular beforeFilter method defined in your controller in which you do some stuff. If that is the case don’t forget to call parent::beforeFilter() in it.
Michał Szajbe
19 Mar 09 at 00:24