code tunes

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

Archive for the ‘controller’ tag

Convenient controller’s callback methods with CakePHP

with 4 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 , ,

“Send this page to friend” with polymorphic controller in Rails

with one comment

Last week I wrote about Tableless models in Ruby on Rails, giving an example of Recommendation model which could be used to validate users’ recommendations. Today I will extend that example so that it will be more complete.

What the mentioned solution lacks is a controller handling recommendation actions. Let’s first make Article and Photo models recoommendable ones by defining their associations:

  has_many :recommendations, :as => :recommendable

The models are ready, so let’s create a controller.

class RecommendationsController < ApplicationController
  def new
    if !params[:photo_id].nil?
      @recommedable = Photo.find params[:photo_id]
    elsif !params[:article_id].nil?
      @recommedable = Article.find params[:article_id]
    end
 
    raise ActiveRecord::RecordNotFound if @recommendable.nil?
  end
 
  def create
    @recommendable = Recommendable.new :params[:recommendation]
 
    if @recommendable.valid?
      # send it via e-mail
      flash[:notice] = 'Your recommendations has been processed'
      redirect_to @recommendation.recommendable
    else
      render :action => :new
    end
  end
end

We’re almost there, but there’s more Rails can do for us here. Let’s define some new routes.

# config/routes.rb
  map.resources :recommendations
 
  map.resources :articles do |a|
    a.resources :recommendations
  end
 
  map.resources :photos do |p|
    p.resources :recommendations
  end

Now we’re able to use those pretty helpers in our views that will generate nice-looking URLs (like: article/1/recommendation/new).

<%= link_to 'recommend this article', new_article_recommendation_path(@article) %>

The link above will take us to the ‘new’ action of RecommendationsController. The last thing we need to do is to render the form, so that user can comment his recommendation.

<% form_for @recommendable.recommendations.build do |f| %>
  <%= f.hidden_field :recommendable_type, :value => @recommendable.class %>
  <%= f.hidden_field :recommendable_id, :value => @recommendable.id %>
  <%= f.text_field :email %>
  <%= f.text_area :body %>
  <%= f.submit 'Do it!' %>
<% end %>

There are many more cases of use polymorphic controllers (with normal or tableless models) than sending recommendations. Commenting, ranking, abuse-reporting, tagging… The rule is that if you have a model that is associated to other models via polymorphic association, you should consider using polymorpic controller to manage it. Not only Rails will do much work for you behind the scenes, but the structure of your application will stay clear both inside (reusable code) and outside (nice links).

Written by Michał Szajbe

July 26th, 2008 at 4:06 pm