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.