1

I'm writing a gem for Rails 4 that needs to add some methods to the ApplicationController, and I figure the best way to do that is to write a rake task to open the file, insert the methods I need, then close it. What's the best way to do that so that my methods are within the class definition?

class ApplicationController < ActionController::Base

  def existing_methods
    foo
  end

  # insert my methods here

end

Edit

After trying the suggestions below, but to no avail, I tried following this post and ended up trying the following:

lib/my_engine.rb

require "my_engine/engine"
require "my_engine/controller_methods"

module MyEngine
end

lib/my_engine/controller_methods.rb

module MyEngine
  module ControllerMethods
    extend ActiveSupport::Concern

    def foo
      "foo"
    end

  end
end
ActiveRecord::Base.send(:include, MyEngine::ControllerMethods)

When running the app, my application.slim includes the line = foo, and I get the following error:

undefined local variable or method `foo' for #<#:0x007ff350cd4ba0>

Community
  • 1
  • 1
Jonathan Bender
  • 1,911
  • 4
  • 22
  • 39
  • You want your gem to literally edit application_controller.rb? Wouldn't it be better to extend ApplicationController at runtime by reopening the class in your gem (or is this not applicable for whatever your gem does?) You might try scanning up from the bottom of the file looking for an 'end' that is matched by 'class ApplicationController'--but I'm sure there are ways that could be broken. (You'd be attempting your own parsing.) – struthersneil Oct 22 '13 at 01:17
  • Yes, I am looking to edit it directly. I may be misinterpreting what you mean by 'extend at runtime', but Rails overwrites the controllers if one with the same name exists in the application, so I can't just write the methods in my engine's controller. – Jonathan Bender Oct 22 '13 at 01:21
  • Consider the possibility of a class definition that all exists on one line. Or some awkward but valid case where ApplicationController is defined via a call to Class.new() – struthersneil Oct 22 '13 at 01:23
  • 1
    Consider making calls to ApplicationController.define_method(...) or something along those lines. Or look into monkey-patching your own methods into the 'live' class at runtime. – struthersneil Oct 22 '13 at 01:27
  • Take a look here: http://stackoverflow.com/questions/7936023/including-methods-to-a-controller-from-a-plugin – struthersneil Oct 22 '13 at 01:30
  • The linked question covers Rails 2.3, I'm using Rails 4, sorry, I should have specified that in the question. – Jonathan Bender Oct 22 '13 at 01:40
  • You're right, there are probably more up to date examples--but the concept is still the same. You essentially make a module that you cause ApplicationController to 'extend itself' with (or include) at runtime. Should be some canonical examples out there. – struthersneil Oct 22 '13 at 01:44
  • I've updated my question to include my first attempt at what I think you're suggesting, but it's resulting in an `uninitialized constant ApplicationController` – Jonathan Bender Oct 22 '13 at 02:01
  • It's 3am, otherwise I'd fire up an editor and try a few things out :) Your gem is loaded before ApplicationController which is possibly why you have this issue. That, or ApplicationController doesn't exist in the :: scope from your gem's point of view. – struthersneil Oct 22 '13 at 02:06

1 Answers1

3

Ok, we're agreed that altering your host app's application_controller.rb isn't the way to go. Let's look at different ways of adding methods to the ApplicationController class (actually ActionController::Base) via a gem.

I've created a very simple gem. I want it to add one function, rot13, that means any controller will be able to call rot13('something!') to get back 'fbzrguvat!'. (In real life you'd add this to String...)

You could extend ActionController::Base, like this:

class ActionController::Base 
  def rot13 str
    a = 'a'.ord
    z = 'z'.ord   
    str.unpack('c*').map { |x| (a..z).cover?(x) ? (((x - a) + 13) % 26) + a : x }.pack('c*') 
  end
end

And now in my application I can call rot13('ibvyn!') inside any controller and voila!

It's safer to add a module and include it in ActionController::Base via a Railtie hook. So let's add a Railtie.

I add lib/rot13/railtie.rb as follows:

module Rot13
  class Rot13Railtie < Rails::Railtie
    initializer "rot_13_railtie.extend_action_controller" do  
      ActiveSupport.on_load :action_controller do
        # At this point, self == ActionController::Base
        include Rot13::ControllerMethods
      end
    end
  end
end

Now lib/rot13.rb looks like this:

require "rot13/version"
require "rot13/railtie.rb" if defined? Rails

module Rot13
  module ControllerMethods
    def rot13 str
      a = 'a'.ord
      z = 'z'.ord   
      str.unpack('c*').map { |x| (a..z).cover?(x) ? (((x - a) + 13) % 26) + a : x }.pack('c*') 
    end
  end 
end

This is fine for most purposes.

Let's say you didn't want your rot13 method to be defined in ActionController::Base and available to all controllers--let's say you wanted users of your gem to 'opt in' on a controller-by-controller basis, e.g.

class ApplicationController < ActionController::Base
  with_rot_13

  # and so on...
end

Instead of include Rot13::ControllerMethods you might call extend Rot13::ControllerOptIn within that on_load block to add a with_rot_13 method at the class level of ActionController::Base, then define a ControllerOptIn module as follows:

module Rot13
  module ControllerMethods
    # ...
  end 

  module ControllerOptIn
    def with_rot_13
      include ControllerMethods
    end
  end
end

That's it!

Edit: just to address the additional question of 'why isn't the method visible in my view?'--your method is not defined as a helper, so it's not automatically visible in your views without suffixing it with controller, e.g. %p= controller.rot13 'blah'. Luckily you can define it as a helper with a call to helper_method, e.g.

module ControllerOptIn
  def with_rot_13
    include ControllerMethods
    helper_method :rot13
  end
end

Now you can do this (note: HAML):

%p= controller.rot13 'hello there!'
%p= rot13 'well how are ya?'

But it's not great to have to specify helper_method :rot13 here. Can you dig the necessary symbols straight out of Rot13::ControllerMethods? Sure, as long as you're sure it's what you want:

module ControllerOptIn
  def with_rot_13
    include ControllerMethods
    helper_method *ControllerMethods.instance_methods
  end
end
struthersneil
  • 2,700
  • 10
  • 11
  • I've updated my question to include my latest trials, while the module is being loaded, the methods don't appear to be available, whether I specify them as instance or class methods. – Jonathan Bender Oct 22 '13 at 16:14
  • Prefix your call with controller, e.g. `controller.foo` if you are in a view. – struthersneil Oct 22 '13 at 16:25
  • The class in the error was the clue--`undefined local variable or method foo for #<#:0x007ff350cd4ba0>`--that would have been `#` if `foo` was called against your controller (and it somehow wasn't there) – struthersneil Oct 22 '13 at 16:28
  • The prefix didn't change anything either. The method isn't tied to anything, it's the equivalent to `<%= foo %>` in erb. – Jonathan Bender Oct 22 '13 at 16:38
  • When you are in the context of a view you are actually in an instance of an anonymous class and not an instance of a controller. (You have access to copies of instance variables of your controller which can be misleading.) Try going back to the approach I've outlined here (which works, I promise!) and calling `Rails.logger.debug(foo)` from within your controller and observe the development log. – struthersneil Oct 22 '13 at 16:44
  • `NameError (undefined local variable or method 'foo' for #):` – Jonathan Bender Oct 22 '13 at 17:05
  • Okay! Now that means something is wrong in the initialization steps of your gem, or your gem is somehow not included in your application. Try this--drop some `puts 'here!'` statements into the body of your modules and also the ActiveSupport.on_load block in the Railtie. Then open your rails console and you should see your messages appear. If not: something's up. – struthersneil Oct 22 '13 at 17:08
  • I've tried going for your last option, but now I'm getting the following `NoMethodError: undefined method 'helper_method' for MyEngine::ControllerExtension:Module` – Jonathan Bender Oct 22 '13 at 18:54
  • Just remember that `helper_method` will only work in the context of ActionController::Base (or something inheriting from it), so use it in exactly the same place you are making your call to `include`. Actually--you might be calling `include` in the wrong place if the calls are already together and you are seeing this error (obviously `include` works everywhere, but `helper_method` will only work inside the correct context!) – struthersneil Oct 22 '13 at 19:50