Archive for the ‘callbacks’ tag
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.
