views:

475

answers:

1

Hi,

I'd like to have a more dynamic way of working with filter chains in Ruby on Rails 2.3.2.

At the moment, the filter chain is built as the classes are loaded. I have a very particular need for the filter chain to be rebuilt at some point in time after the classes have loaded.

This is to do with a Rails project (Spree, Radiant may also be affected I'm guessing) that uses extensions which can do things like this after the filter chain has been built for MyController:

MyController.class_eval do
  before_filter :my_filter_method
end

ApplicationController.class_eval do
  before_filter :app_filter_method
end

The problem here is that the app_filter_method filter will not be added to the MyController filter chain. This is because the filter chain of MyController is built from an earlier copy of the filter chain of ApplicationController. This copy of the filter chain of ApplicationController does not yet have the app_filter_method filter applied to it.

I can think of 2 places so far that the rebuilding of the FilterChain could happen in:

1) Each time it is called on MyController.filter_chain

2) Reloadable on demand. So MyController.reload_filter_chain will rebuild the filter chain, using the filters from MyController's subclasses in the chain.

Perhaps specifying a subclass of FilterChain like ReloadableFilterChain that builds the filter chain for each request might do it - thoughts?

Here are links to the source of filters.rb on GitHub, for Ruby on Rails 2.3.2:

The filter_chain method (line 573)

The FilterChain class (line 10)

I was hoping some of you here might have some insight or advice on how to do this.

Any help much appreciated.

Eliot

More detail as requested:

Spree is a rails app that uses extensions to modify behaviour.

Sometimes an extension is used to add a filter to a controller (such as in the example code in the question). One example use is the static content extension.

The static content extension allows you to show HTML pages stored in the database to be shown for any request path you specify. For example, you could show one of these HTML pages instead of the default content Spree shows for the /products request path.

The static content extension filters all requests and checks the path. If the path matches one of the pages in the db, then the filter renders the page.

The static content extension declares its filter like so:

Spree::BaseController.class_eval do
  before_filter :render_page_if_exists

  def render_page_if_exists
    ...
  end
end
A: 

Back in Rails 1.0 the filter chain didn't used to get cached like this and so wouldn't have been a problem.

The reason the filter chain became cached was for performance. There's little reason not to cache it. From searching around for other people wanting it to be dynamic I found only one post. So it looks like there is rarely any need for the filter chain to be dynamic or reloadable after the app has started unless you get a case like this.

Bearing this in mind, I think a method like FilterChain.reload would be a good way to keep the performance gains.

In Spree FilterChain.reload could be called after loading the extensions. I prefer this to the solution I suggested in an earlier comment (patching before_filter) as I think its got a better shot of being useful to Rails core.

FilterChain.class_eval do

  # Reloads all filter chains on all controllers, working from the ApplicationController down
  # through the class hierarchy.  e.g. Spree::BaseController would get its filter chain reloaded
  # before its subclasses like ProductsController. We do this as ProductsController's filter chain
  # relies on a copy of the Spree::BaseController filter chain.
  def self.reload
    # ApplicationController.filter_chain does not need reloading, it will be the only correct
    # filter chain for sure as it is not inherited from a superclass.
    reload_child_filter_chains(ApplicationController)
  end

  def self.reload_child_filter_chains(controller_class)
    controller_class.immediate_subclasses.each do |controller_child|
      # Reload filter chain on each controller who's immediate parent is the controller_class
      controller_child.filter_chain.merge_filter_chain(controller_class.filter_chain)
      # Reload the children of controller_child
      reload_child_filter_chains(controller_child)
    end
  end

  # New instance method on FilterChain to merges the given parent chain into itself.
  def merge_filter_chain(parent_chain)
    # Compare self and parent_chain and insert any parent_chain filters that
    # are missing from self.  You may need special handling for
    # Filters that were marked for skipping or with :only, etc. conditions.
    ...
  end
end

Taken from comments here:

Spree issue 653 Specifying filters in extensions can create filter chains missing filters

Eliot Sykes