How can I extend ApplicationController in a gem?
Asked Answered
U

4

19

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 of defining a before_filter and after_filter to and use initialize to access the class's name to determine the associated model's class that it could then store and use within its methods?

Update 2012/10/22:

Here's what I came up with:

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

    # note: don't specify included or ClassMethods if unused

    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
        before_filter :some_instance_method_available_on_every_controller # to be available on every controller
        after_filter :another_instance_method_available_on_every_controller # to be available on every controller
        include FantasticStuff
      end
    end

    # instance methods to go on every controller go here
    def some_instance_method_available_on_every_controller
      puts "a method available on every controller!"
    end

    def another_instance_method_available_on_every_controller
      puts "another method available on every controller!"
    end

    module FantasticStuff
      extend ActiveSupport::Concern

      # note: don't specify included or ClassMethods if unused

      included do
        class_attribute :class_attribute_only_available_on_fantastic_controllers, instance_writer: false
      end

      module ClassMethods
        # class methods available only if make_this_controller_fantastic is specified in the controller
        def some_fanastic_class_method
          put "a fantastic class method!"
        end
      end

      # instance methods available only if make_this_controller_fantastic is specified in the controller
      def some_fantastic_instance_method
        puts "a fantastic instance method!"
      end

      def another_fantastic_instance_method
        puts "another fantastic instance method!"
      end
    end
  end
end
Unknowable answered 5/7, 2012 at 16:8 Comment(0)
R
5

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.

Regulus answered 5/7, 2012 at 23:37 Comment(2)
Awesome! Including a module is really the best idea, then. Thanks a lot for your help!Unknowable
Related conversation in Rails ForumUnknowable
X
10

For this specific kind of functionality I would recommend creating a module in your gem and include that module in your Application Controller

class ApplicationController < ActionController::Base
  include MyCoolModule
end

To add before filters, etc (add this to your module)

def self.included(base)
  base.send(:before_filter, my_method)
end

Update: you may be able to just do base.before_filter :my_method which is cleaner.

Xanthochroid answered 5/7, 2012 at 16:11 Comment(2)
Would including a module really work in this case? Could you expand on how you would get the subclass's name since there is no subclass, and how you would take advantage of before_filter and after_filter? Take another look at the question and if you can expand on those, please clarify. Otherwise, I'll assume it isn't an option. Thanks!Unknowable
Just clarified in the last line of the question that I need to use before_filter, after_filter, and access the subclass's name.Unknowable
R
5

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.

Regulus answered 5/7, 2012 at 23:37 Comment(2)
Awesome! Including a module is really the best idea, then. Thanks a lot for your help!Unknowable
Related conversation in Rails ForumUnknowable
C
2

Truth is much much simpler and flexible.

Add to lib/engine.rb this: class Engine < Rails::Engine; end

And then simply use:

ActionController::Base.class_eval do

  include SomethingFromMineGemModule

  # or:
  def hello_from_gem
    'Hey people!'
  end

end
Cavan answered 24/10, 2016 at 17:30 Comment(0)
P
0

I was able to reference ApplicationController with an initializer callback.

gem code that subclasses/references ApplicationController:

class GemApplicationController < ApplicationController
  before_filter :method_to_call

  def method_to_call
    #your code here
  end
end

gem code callback to create subclassed controller:

module GemName
  def self.load_gem_application_controller
    require "path/to/gem_application_controller"
  end
end

rails_app/config/initializers/gem_name.rb

GemName.load_gem_application_controller

Then have controllers that use this functionality subclass GemApplicationController

class SpecialCaseController < GemApplicationController
  # this will inherit from the gem's controller, 
  # which inherits from the rails_app ApplicationController
end
Postbellum answered 14/6, 2013 at 19:58 Comment(2)
I learned that it is better to use modules. You can only inherit from one parent class (directly), but you can include/extend modules as much as you want.Unknowable
Thanks Gary. I ended up implementing with a module as well.Postbellum

© 2022 - 2024 — McMap. All rights reserved.