code tunes

Web applications, software engineering, Ruby on Rails, Cake PHP, JavaScript, etc.

Archive for the ‘callbacks’ tag

Convenient controller’s callback methods with CakePHP

with 6 comments

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.

Written by Michał Szajbe

August 21st, 2008 at 12:50 am

Posted in CakePHP

Tagged with , ,