Archive for the ‘controller’ 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.
“Send this page to friend” with polymorphic controller in Rails
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).
