Topic: Subclassing ApplicationController in a gem

(also posted in http://stackoverflow.com/questions/1134 … r-in-a-gem )

I thought I'd come up with a slick way to extend ApplicationController in a Rails 3.x gem.

In my gem's lib/my_namespace/my_controller.rb, I had:

class MyNamespace::MyController < ApplicationController

  before_filter :some_method
  after_filter :another_method

  def initialize
    # getting classname of the subclass to use for lookup of the associated model, etc.
    # and storing the model_class in an instance variable
    # ...
  end

  # define :some_method, :another_method, etc.
  # ...

private
  attr_accessor :subclass_defined_during_initialize # etc.

  # etc.
end

but when the Gem is loaded, app/controllers/application_controller.rb is not yet loaded, so it fails:

/path/to/rvm/gemset/gems/activesupport-3.2.6/lib/active_support/dependencies.rb:251:
in `require': cannot load such file -- my_gem_name/application_controller (LoadError)

As a workaround, I had defined ApplicationController in my gem's lib/gem_namespace/application_controller.rb as:

class ApplicationController < ActionController::Base
end

I assumed that even though I had defined it there, it would be redefined in my Rails 3 application's app/controllers/application_controller.rb, such that both controllers in the application that extended ApplicationController and controllers that extended MyNamespace::MyController would directly or indirectly extend the ApplicationController defined in app/controllers/application_controller.rb.

However, we noticed that after loading the gem, controllers that extend ApplicationController were unable to access methods defined in app/controllers/application_controller.rb. Also, the ApplicationHelper (app/helpers/application_helper.rb) module was no longer being loaded by other helper modules.

How can I extend ApplicationController within the controller in my gem for the purpose allowing others to subclass my controller that subclasses ApplicationController, so that I can use before_filter, after_filter, and use initialize to access the subclass's name to determine the associated model's class that it is then storing in a private accessor on the subclass?

Last edited by gsw (2012-07-05 13:54:55)

Re: Subclassing ApplicationController in a gem

Give this article a read http://www.cowboycoded.com/2011/02/11/w … -3-engine/

Re: Subclassing ApplicationController in a gem

Thanks! I looked into that but it also seems stuck in the idea of includes. Although I am just trying to add methods to a controller and use hooks like before_filter and after_filter, and I think including a module would be the clearest and most flexible route in a lot of ways, I need to have different behavior for the injected methods depending on the name of the instantiating class. I tried a number of "meta" ways to inject methods and then add variables, etc. dynamically, and the cleanest method and the one that worked (kind of) was just subclassing.

What I want is for the applications's ApplicationController to be loaded before my gem's controller because the controller in my gem subclasses it, and the other application controller(s) that subclass the controller in my gem to be loaded after that.

So the simplest and cleanest approach I think is for loading the gem to not load the controller classes in the gem, and then just do a require in the top of every controller in the application that requires it. It is definitely not as clean as I'd hoped, because there will be an additional require line in every controller.

However, the other thing I was thinking about was that subclassing ApplicationController is limiting. What if the user wants to subclass a subclass of ApplicationController? I'm sure that is one of the many reasons doing an include is better.

It also would seem like something like the following would be a really neat addition to Ruby:

public class MyController < ModuleInMyGem::ControllerInMyGem < ApplicationController
  # ...
end

and define my gem's controller like:

public class ModuleInMyGem::ControllerInMyGem < ClassToBeDetermined
end

and then it would be smart enough to not try to load ModuleInMyGem::ControllerInMyGem until ApplicationController was loaded... somehow. Probably really bad things would happen by way of that. Seems like a nice idea at the moment though, given little thought.

Re: Subclassing ApplicationController in a gem

Posted a response on Stackoverflow: http://stackoverflow.com/questions/1134 … es#tab-top

Re: Subclassing ApplicationController in a gem

Awesome!

Just as a backup, posting your answer/link to gist and gist contents here, just in case stackoverflow and/or github go down and someone's looking for it.

Here is a Gist that shows how to access the class of the subclass and store it in an instance variable and access it in the before and after filters. It uses the include method.

# I created a generic Rails like ApplicationController so that you can just
# run this code via a command line prompt without the need for Rails


module Recorder
  
  def self.included(base)
    base.send(:before_filter, :some_method)
    base.send(:after_filter, :another_method)
  end
  
  def initialize
    super
    #self references the instance of the UsersController class
    @model_class = self.class
    puts @model_class # UsersController
  end
  
  def some_method
    puts 'from before filter method'
    puts @model_class # UsersController
  end
  
  def another_method
    puts 'from after filter method'
    puts @model_class # UsersController
  end
  
end

class ApplicationController

  #stubbing out some stuff
  @@before_filters = []
  @@after_filters = []
  
  def self.before_filter(method)
    @@before_filters << method
  end
  def self.after_filter(method)
    @@after_filters << method
  end
  
  def run(filter)
    self.class.class_variable_get(('@@'+filter).intern).each do |method|
      send(method)
    end
  end
  
  include Recorder
end

class UsersController < ApplicationController; end


controller = UsersController.new

controller.run("before_filters")

controller.run("after_filters")

(posted by th3mus1cman to StackOverflow)

Re: Subclassing ApplicationController in a gem

BTW- in response to my own "neat addition" above, I could dynamically define classes that extend whatever class I wanted easily in Ruby and could slip that into the load order by just doing a require, etc. in the appropriate place that defined it.

I even read yesterday that with evil-ruby and in a fork of 1.9 (and only in MRI, I think), even the class inheritance can be changed after the fact, but that is not recommended, which is why it is with "evil" that it must be done. smile http://stackoverflow.com/questions/3127 … ce-in-ruby

But, I'll do it as a module, and now I understand how the post you mentioned is a lot more helpful: http://www.cowboycoded.com/2011/02/11/w … -3-engine/

Thanks!

Re: Subclassing ApplicationController in a gem

Just added the following note here also, because I don't want people going off on wild goose chases trying to extend ApplicationController when it is not a good idea, at least not in Rails 3.x:
http://stackoverflow.com/questions/1134 … r-in-a-gem

Here's the update (as of 2012/08/31):

I thought I wanted to extend ApplicationController because subclassing ApplicationController seemed more elegant and OO. But, I ended up using a railtie in my gem that included a module using ActiveSupport::Concern. That module adds a class method in its ClassMethods module to ActionController::Base. That class method, if called by the user's controller, includes a module of instance methods, not called InstanceMethods, as those are included by ActiveSupport::Concern per deprecated behavior, and specifies before_filter and after_filter.

In lib/your_gem_name/railtie.rb:

module YourGemsModuleName
  class Railtie < Rails::Railtie
    initializer "your_gem_name.action_controller" do
    ActiveSupport.on_load(:action_controller) do
      puts "Extending #{self} with YourGemsModuleName::Controller"
      # ActionController::Base gets a method that allows controllers to include the new behavior
      include YourGemsModuleName::Controller # ActiveSupport::Concern
    end
  end
end

and in lib/your_gem_name/controller.rb:

module YourGemsModuleName
  module Controller
    extend ActiveSupport::Concern

    included do
      # anything you would want to do in every controller, for example: add a class attribute
      class_attribute :class_attribute_available_on_every_controller, instance_writer: false
    end

    module ClassMethods
      # notice: no self.method_name here, because this is being extended because ActiveSupport::Concern was extended
      def make_this_controller_fantastic
        extend YourGemsClassMethods
        include YourGemsInstanceMethods # intentionally not just InstanceMethods as those would be automatically included via ActiveSupport::Concern, which is deprecated
        before_filter :some_method
        after_filter :another_method
      end
    end

    module YourGemsClassMethods
    end

    module YourGemsInstanceMethods
      # instance methods to include only if make_this_controller_fantastic is specified in the controller
      def some_method
        puts "some method"
      end

      def another_method
        puts "another method"
      end
    end