Routing Polymorphic Resources in Rails

Routing in Rails is a breeze, but when dealing with polymorphic resources it is less than polished. Consider this example: an eCommerce application consists of products, which I can view in the scope of the store, a category or my shopping cart. This would require the following routes:

/products/1
/category/2/products/1
/cart/3/products/15


This is simple to represent, but how do we handle these scopes in the controller? The accepted solution seems to be:

  def find_products
    if params[:category_id]
      @p = Product.find :all, :conditions => { :category_id => params[:category_id] }
    elsif params[:cart_id]
      @p = Product.find :all, :conditions => { :cart_id => params[:cart_id] }
    else
      @p = Product.find :all
    end
  end


This is an ugly solution. What if I now want to find products in the scope of a Supplier, or an Order? This makes highly coupled code, that will be hard to maintain.

The Solution

The solution starts in the controller. We need to find the scope dynamically:

  def find_scope
    if params[:productable_type]
      @scope_class = params[:productable_type].singularize.classify.constantize
      @scope = @scope_class.find params[:productable_id]
    else
      @scope_class = nil
      @scope = Product
    end
  end


Then, we find products in the dynamic scope:

  def find_products
    @products = @scope.find :all
  end

  def find_product
    @product = @scope.find params[:id]
  end


But where does the productable_type parameter come from? The routes file.

Routing the Resource

In the routing file, we can now set up the routes to the products controller:

  map.resources :products

  map.resources :categories do |category|
    category.resources :products, :path_prefix => ':productable_type/:productable_id'
  end

  map.resources :carts do |cart|
    cart.resources :products, :path_prefix => ':productable_type/:productable_id'
  end


This looks like perfectly normal nested resources, but we defined a path_prefix on the instances of products. Why? It allows us to capture more of the URL. If we use rake routes to print out the routing table, we find:

  /:jobable_type/:jobable_id/jobs


When we make a request to /categories/2/jobs, the parameters hash contains:

  Parameters: {"productable_id"=>"2", "productable_type"=>"categories"}


Using the methods we defined in the controller, the scope is found and the products are found in the correct scope.

Using Named Routes

We have solved the routing problem, but there’s a problem. I want to use named routes, such as category_products_path(2) to link to the products page of category 2. The problem is, calling this raises an exception, because Rails is expecting the new productable_type variable to be passed.

I have yet to find a perfect solution to this. A reasonable suggestion is to define a library to override routes.

module PolymorphicRoutes
  def category_products_path(*args)
    super 'category', *args
  end

  def category_product_path(*args)
    super 'category', *args
  end
end


This automatically adds the ‘category’ argument to the start of the path.

To make this available to both controllers and views, we include it twice: once in ApplicationController and again in ApplicationHelper.

Polymorphic Routing is solved.

Trackback URL

, ,

Comments are closed.