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.

Custom thumbnail generation with Paperclip

Paperclip is a great plugin for Ruby on Rails which eases the pain of image upload and resize process. The usage is very simple, results are what you want in most cases. Not in all cases though.

Your role is only to define the sizes of thumbnails that will be generated from original image. Thumnailing in is fact done by calling ImageMagick’s convert command with -scale argument, for example convert -scale ’640×480>’.

You can alter how exactly the image is scaled by appending modifiers to desired thumbnail size, like ‘>’ in ’640×480>, however it won’t give you unlimited power over thumbnailing process.

Consider following example. I have a small image (120×120).

Scaling it to smaller size gives me the result I want. But when the result image needs to be bigger than original, the original image is enlarged in a way that you’ll see single pixels.

What I want to achieve is the enlargment done not by scaling the original image, but by adding a border around it.

This cannot be achieved by adding any of modifiers to desired thumbnail size, so why not to pass additional arguments to convert command? Yes, that would be a solution.

And here’s my solution – PaperclipExtended. It’s a plugin that modifies original Paperclip, so that now it accepts additional (optional) parameter :commands when defining the thumbnails sizes.

class User &lt; ActiveRecord::Base
  has_attached_file :avatar, :styles =&gt; { :medium =&gt; "300x300&gt;", :small =&gt; "100x100&gt;" },
    :commands =&gt; { :medium =&gt; "-background white -gravity center -extent 300x300 +repage" }
end

During thumbnail generation Paperclip will now append given commands to convert command. Convert command for medium style will be now:
convert -scale ’300×300>’ -background white -gravity center -extent 300×300 +repage
Convert command for small style will remain unchanged.

Passing such commands will help me with the enlargment I mentioned above. For what else could be done check the documentation of ImageMagick’s command line options.

PaperclipExtended is not a replacement for Paperclip, just an extension. It works with version 2.1.2 of Paperclip plugin (the current one and the only one I tested). Compatibility with future version is not guaranteed.

I put a plugin on GitHub.
http://github.com/netguru/paperclip-extended

How to install:

script/plugin install git://github.com/netguru/paperclip-extended.git

Configuration is the same as of original Paperclip plugin, but you can now add an optional :commands parameter.

Hope it’ll be helpful!

If you have any questions, post them in comments to this post.

“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).

Tableless models in Rails

If you’re developing an application in Ruby on Rails framework, there are many situations when you should consider using tableless models. The advantages you’d get are:

  • consistent application structure, because you’re using models to represent objects in your app
  • routes available for free if you also define controllers dedicated for those models, resulting in RESTful application
  • easy validation (just like with normal models) and other goodies shipped with Active Record
  • easier testing with unit tests
  • form building as easy as with normal models

If you’re familiar with “fat model, skinny controller” concept you’ll find more reasons.

An example situation: you give the users an ability to recommends object they find on the site to their friends. They can recommend photos and articles. You do not want to track those recommendations in database. Your model file would look like this.

class Recommendation < ActiveRecord::Base
  class_inheritable_accessor :columns
  self.columns = []
 
  def self.column(name, sql_type = nil, default = nil, null = true)
    columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
  end
 
  column :recommendable_type, :string
  column :recommendable_id, :integer
  column :email, :string
  column :body, :text
 
  belongs_to :recommendable, :polymorphic => true
 
  validates_presence_of :recommendable
  validates_associated :recommendable
  validates_format_of :email, :with => /^$|^\S+\@(\[?)[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,4}|[0-9]{1,4})(\]?)$/ix
  validates_presence_of :body
end

As you can see the only difference in model definition is that you need to provide the columns yourself.

The idea was borrowed from this snippet.

In the next article I’ll cover polymorphic controllers which will help to convert above example into complete one.

[edit: Thanks to Curran Schiefelbein for making this work when tableless model is inherited by another model.]

Migration to CakePHP RC2

So far one of my app was running on 1.2.0.6311 version released on 2 Jan 2008. It worked fine, and newer version could break something, so I didn’t bother to upgrade. However now, when new functionalities are to be implemented, I finally decided to migrate (to 1.2.0.7296 RC2). Of course some things stopped working as I expected, so I decided to list them here for others.

  • custom validation methods defined in a model must be public now
  • App::Import changed again, so my post about switching from vendors() to App::Import from March 2008 is not actual anymore
  • structure of core.php file was changed a little, so configuring the file from scratch would be a good idea
  • some ‘deprecated’ warnings, easy to correct

Well that is not much, even for such a quite simple application. Honestly, I expected to face more problems. Hopefully they don’t remain undiscovered.

Not so obvious behavior of $uses in CakePHP

In CakePHP you declare what models you’re going to use in a controller by defining $uses variable. You can also define it in AppController to have a certain set of models available in every controller. However be careful when doing it because you may put yourself in an unexpected situation as I did recently.

I need an access to User model in beforeFilter and beforeRender actions on every request so I defined those actions in AppController along with:

$uses = array('User');

When I did this I found out that my SessionsController which handles log in and log out logic stopped working immediately. There is no Session model in the app, SessionController works with User model instead:

$uses = array('User');

The error said: “Database table sessions for model Session was not found”. So it tried to find Session model which doesn’t exist as I stated earlier. But why? I have not told Cake to include anything but User model.

To see what exactly has happened we need to look how Cake determines which models it needs to include and it’s done by merging AppController’s $uses with SessionController’s $uses. Merging is done in usual intuitive way with one exception: when SessionController’s $uses is equal to AppController’s $uses then SessionController’s “native” model (Session model here) is also loaded.

Do you have a rational explanation for such behavior? I have none. It’s very unintuitive for me.

Note that the equality of both controllers’ $uses can happen in two situations:

  • both controllers have $uses defined explicitely and they’re equal
  • SessionController doesn’t have $uses defined at all and derives it from AppController (parent)

When SessionController is (===) null or false no models are loaded at all, no mather what’s been defined in AppController.

Formatting dates for SQL queries in Rails

Here’s a simple and clean way to format date for use in SQL query in Ruby On Rails.

User.find :all, :conditions => ['created_at > ?', 7.days.ago.to_formatted_s(:db)]

You can use to_formatted_s(format) method with different data types – Date, Time, DateTime, Range, Array and BigDecimal. There are also more formats available apart from :db. Find out more in Rails API.

Problematic SwfUpload

SwfUpload is a nice and easy to use javascript/flash library that resolves the problem of uploading many files at once to the server.

There are however some issues that sometimes make the experience of using the library a painful one. The problem come not from the SwfUpload itself, but rather from flash plugins installed on the browsers. Especially older versions.

One of the problem is that some of the plugins (Adobe’s Flash < 9.0 if I remember correctly) doesn't pass cookies during the upload, which results in lost session. Detailed description and possible solutions can be found around the web, for example here.

The other problem I came accross was that an user got logged out every time he uploaded a file with IE6. It was caused by User Agent mismatch when restoring session, so User Agent check had to be disabled for upload requests.

When it comes to using SwfUpload on Mac some new issues appear. One is a subdomain problem that causes a 404 error.

Couple of days ago I was hit by an error that took me all day to cope with. It was none of the above, the request seemed to be all correct, input data passed to an action (CakePHP application) was complete. The action itself didn’t work though. After quite painful debugging I found out that there was a port number appended to host name in $_SERVER['HTTP_HOST']. For an usual configuration it should not make any difference, however here it did and an absence of database connection was that difference.

The application gets deployed to more than one server and database it connects to depends on the server. The code that determines database hostname relied on the $_SERVER['HTTP_HOST'] variable and didn’t care about port numbers. As it appeared it should have!

I didn’t see such a problem described anywhere else, so I decided to share. Hopefully this can help someone few hours of work.

After RuPy’08

RuPy logoRuby and Python Conference (RuPy) took place in Poznań, Poland this weekend. It was second edition of the RuPy and I must say that the event was great.

I have not written a single line of code in any of those languages before, but I was seriously considering switching to Ruby (and Rails) soon. I am fortunate enough that next project I will be working on will be written in Ruby on Rails, so this will be even bigger motivation to learn it. And I must say that after the conference my motivation is sky high.

There were many interesting talks, for me, rails-developer-wannabe however the most interesting was live-coding session given by Rida Al Barazi, who was trying to answer the question “Is Rails as agile as advertised”. I saw him in action for about two hours as he was developing simple movie database web application and I admit, it’s very agile indeed. At least when you compare it to Cake, which I like very much when I develop in PHP.

Other lectures was I liked was : “Test driven developement in Rails” by Andrzej Krzywda, “Correlations and Conclusions” by Zed Shaw, “Business Natural Languages” by Jay Fields and “Caching in Rails” by Wiktor Schmidt.

Python part of the conference was taking place at the same time in another room. I didn’t attend it as I decided to focus on Ruby. Maybe next year.

Videos and other materials from RuPy should be available soon on conference’s website.

Surely I will be posting here about Ruby too in future. Of course, I am not going to give up Cake completely anytime soon. There are several Cake-powered projects I am still involved in.

Switching to App::import() may cause problems

Some time ago importing 3rd party libraries through vendor() method became deprecated and now usage of App::import(‘Vendor’, ‘…’) is suggested. I decided to walk through my application files and switch to new version. It was not that easy however… In my app I use Swift Mailer library for sending e-mails.

// I changed this...
vendor('Swiftlib/Swift/Connection/SMTP');
// to this...
App::import('Vendor', 'Swiftlib/Swift/Connection/SMTP');

It didn’t work, because Cake’s Inflector translated the path to swiftlib/swift/connection/s_m_t_p.php. Of course such file does not exist. Changing vendor’s file names wouldn’t be a good idea. After checking the API I found that App::import() also takes $file parameter if you want to explicitly specify which file to include. So the working solution looks like this:

App::import('Vendor', null, true, array(), APP.'vendors/Swiftlib/Swift/Connection/SMTP.php');

Remember that when you use 3rd party libraries and they do not follow Cake’s conventions in naming files.