Not logged in (Log in or Sign up)

unwwwritten

Bend it to your will - Episode III

In order to hook our plugin up to the ApplicationController (or any other controller class) we currently need to include the SimpleNav module into our class:

include SimpleNav

Pretty simple, but still one line of code added to SimpleLog means that I can't just update the application to a new version without redoing that change (and any others that I've made). Also, if you install my plugin into SimpleLog, you need to update your copy of the ApplicationController too.

Now, if I were extending ActionController::Base we could deal with this part of the process by automatically including SimpleNav from within the plugin (as many plugins do) by adding the following to the end of lib/simple_nav.rb:

ActionController::Base.class_eval do
  include SimpleNav
end

This might work, but would mean that every controller class in our application would install our filter (and every controller action would load the page array).

What if we change the class from ActionController::Base to ApplicationController? Then, at least it's only those controller classes... but the entire SimpleLog admin section does not need to use SimpleNav. Plus, as you'll see in the following brief discussion of loading, it's not quite that simple anyway.

A Quick Look at Loading

Plugins are usually used to add functionality to Rails so that you application can then use it. In this case, we're instead adding functionality to the application controller. The order in which things are loaded makes this a bit more challenging than a standard plugin.

Thanks to Rick Olson, we know that the "secret to all this is kept in Rails::Initializer#process."

Most importantly, the basic order that things are loaded is:

  1. Framework
  2. Environment
  3. Plugins
  4. Application

This explains why the plugin can't just include its module into the ApplicationController during initialization. The Application Controller class has not been loaded yet while the plugin is loaded.

Hooked on Ruby

Every time I work with Ruby I'm amazed/amused by something. If you dig around through the Rails framework code (and give Programming Ruby a pretty good read) you'll find out that (in my opinion) it's Ruby that makes Rails possible. One way that Ruby makes many of Rails tricks possible is through callbacks.

There are eight callbacks that allow you to "hook" a number of system events. By default they do nothing, but if you define them in your classes you can do any number of things. Of course, since we're using Rails (and not just Ruby) we have to be careful not to break the framework if we do use these functions.

  1. Module#method_added - called when an instance method is added
  2. Module#method_removed - called when an instance method is removed
  3. Module#method_undefined - called when an instance method is undefined
  4. Kernel.singletonmethodadded - called when a singleton method is added
  5. Kernel.singletonmethodremoved - called when a singleton method is removed
  6. Kernel.singletonmethodundefined - called when a singleton method is undefined
  7. Class#inherited - called when a class is subclassed
  8. Module#extend_object - called when a module is mixed in

In our case, I want to add a before filter to some subclasses of ActionController::Base. This would seem to be a case for a Class#inherited callback. We can replace our included hook from my previous post with the following:

module SimpleNav
  def self.included(base)
    base.class_eval do
      class << self
        def inherited_with_page_loader(sub_class)
          inherited_without_page_loader(sub_class)
          return unless sub_class.name == 'ApplicationController'
          sub_class.class_eval do
            def preload_pages
              return unless %w(post page).include?(self.controller_name) or self.action_name == 'handle_unknown_request'
              @pages = Page.find(:all, :conditions => 'is_active = true')
            end
            before_filter :preload_pages
          end
        end
        alias_method_chain :inherited, :page_loader
      end
    end
  end
end

Let's see what we're doing here. Previously, we simply defined the preload_pages method and installed it as a before filter.

Now, in order to avoid modifying controller classes not derived from ApplicationController, we instead create a method that does the method definition and before filter installation (the new inheritedwithpageloader method). We then install it as an inherited callback. Whenever a class inherits from ActionController::Base, our callback is invoked. It checks to see if the class in question is ApplicationController and, if it is, mixes in the pageloader method and installs it as a before filter.

A couple more things to point out...

aliasmethod_chain is a really handy method implemented in the Rails framework to help with implementing "method chains".

For example, instead of:

alias_method :foo_without_feature, :foo
alias_method :foo, :foo_with_feature

You can simply:

alias_method_chain :foo, :feature

Lastly, a close look at the implementation of _ preloadpages__ shows an extra line of code that was not in our previous version. Our before filter is installed for the ApplicationController class and (due to further inheritance) all ApplicationController derived classes. We only need the page array loaded for the PostController and PageController, so the first thing the new implementation does is test that the controller name is either "post" or "page".

You may be wondering why we installed the filter for ApplicationController, and not just for PageController and PostController. The answer is that we also needed to load the page array for the ApplicationController#handleunknownrequest action. The aforementioned line of code also tests for that condition.

Now, this may be overkill. I'm the first to admit it. I may never need this as a plugin, and as such, probably should have just modified my personal copy of SimpleLog and gotten on with life. However, I enjoy a good challenge and have learned a lot about plugins in the process.

Cheers.

--Brent

blog comments powered by Disqus