15

I am using a Rails engine as a gem in my app. The engine has PostsController with a number of methods and I would like to extend the controller logic in my main app, e.g. to add some methods. If I just create PostsController in the main app, then the engine's controller is not loaded.

There is a solution proposed in question Rails engines extending functionality based on altering ActiveSupport::Dependencies#require_or_load

Is it the only/correct way to do this? If yes, where do I put that piece of code?

EDIT1:

This is the code suggested by Andrius for Rails 2.x

module ActiveSupport::Dependencies
  alias_method :require_or_load_without_multiple, :require_or_load
  def require_or_load(file_name, const_path = nil)
    if file_name.starts_with?(RAILS_ROOT + '/app')
      relative_name = file_name.gsub(RAILS_ROOT, '')
      @engine_paths ||= Rails::Initializer.new(Rails.configuration).plugin_loader.engines.collect {|plugin| plugin.directory }
      @engine_paths.each do |path|
        engine_file = File.join(path, relative_name)
        require_or_load_without_multiple(engine_file, const_path) if File.file?(engine_file)
      end
    end
    require_or_load_without_multiple(file_name, const_path)
  end
end
Community
  • 1
  • 1
Andrei
  • 9,162
  • 7
  • 68
  • 103
  • You can make this by using a mere class_eval on the controller you want to change – apneadiving Feb 18 '11 at 18:49
  • @apneadiving, if you mean modifying the engine, then I would prefer to avoid this. If you mean smth different, could you explain in more details? – Andrei Feb 18 '11 at 18:55
  • 1
    I think he means using another file and doing a class_eval within this file. PostsControllerExtension would load since the filename is not the same. – johnmcaliley Mar 02 '11 at 01:51
  • I created a gem from this answer about a year ago but forgot to post it here. It works well for us: https://github.com/EPI-USE-Labs/activesupport-decorators – Pierre Pretorius May 21 '15 at 07:19

7 Answers7

11

By design, classes in a Rails::Engine are supposed to be scoped to the engine. That way they don't introduce strange bugs by accidentally stomping all over code loaded in the main app or by other engines. Monkeypatching ActiveSupport::Dependencies to mix engines across-the-board is a really bad workaround.

Just use a Rails::Railtie, instead. They have all the same functionality, but aren't scoped the same way as an engine. You have access to the entire rails app stack (including engines). It's a more surgical approach.

module MyModule

  module SomeModelExtensions
    # Called when this module is included on the given class.
    def self.included(base)
      base.send(:include, InstanceMethods)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def some_new_class_method
        # do stuff...
      end
    end

    module InstanceMethods
      def some_new_instance_method
        # do stuff...
      end
    end

  end

  module SomeControllerExtensions
    def self.included(base)
      base.send(:include, InstanceMethods)
      base.alias_method_chain :new, :my_module
    end

    module InstanceMethods
      # override the 'new' method
      def new_with_my_module
        # do stuff
      end
    end
  end

  class Railtie < ::Rails::Railtie

    # The block you pass to this method will run for every request in
    # development mode, but only once in production.
    config.to_prepare do
      SomeModel.send(:include, MyModule::SomeModelExtensions)
      SomeController.send(:include, MyModule::SomeControllerExtensions)
    end

  end

end

As far as file layout, railties look exactly like engines.

Further reading: Extending Rails 3 with Railties

And if you're still confused, take a look at this git project which has a full implementation: https://github.com/jamezilla/bcms_pubcookie

James H
  • 1,607
  • 20
  • 22
  • 1
    Nice answer on how to extend a Rails app with an engine, but not on how to extend an engine from a rails app – Hugo May 09 '13 at 11:16
7

Why not just inherit from the Engine's controller class in your application (and point your routes at the new child controllers)? Sounds conceptually similar to the way that you extend built-in Devise controllers.

Obie
  • 680
  • 6
  • 16
  • yeap, that would be the most conceptually correct way to do such extension. – Andrei Feb 24 '11 at 10:06
  • 5
    I know this question is targeting controllers specifically, but this problem is present for everything in the /app dir of the engine. This solution does not work as well for models, etc.. when the engine is calling on a specific model name (for example: create in a UsersController calls User). You can't just create a subclass of User, without modifying the engine's code to call that subclass name. I am also not crazy about the idea of rewriting the routes, especially when they are defined in the engine. IMO the AS:Dependencies patch is the best way to go right now... – johnmcaliley Mar 02 '11 at 15:28
  • @cowboycoded can you give an example reference for the "AS:Dependencies patch" reference you made? I am deciding on Rails extension options. – westonplatter Jul 11 '12 at 14:17
  • @westyfresh - look at Method1 from Andrea's answer. You may want to look at this answer also - http://stackoverflow.com/questions/5156061/reopening-rails-3-engine-classes-from-parent-app I am using this approach now. Just requiring the class explicitely in the app that contains the engine. That way, you are guaranteed that the engine class will be reloaded. You also don't have to specify "class SomeClass < SomeSuperclass", just reopen it without the subclass call, like "class SomeClass" – johnmcaliley Jul 11 '12 at 18:33
  • @westyfresh yes, I have been coding an engine & app in the last week that uses this and seems to work fine: https://gist.github.com/3095339 – johnmcaliley Jul 12 '12 at 02:46
4

Method 1

Here is what I put in my Rails 3 app in application.rb after require 'rails/all' (let me know if it is a bad place to put it)

require 'active_support/dependencies'
module ActiveSupport::Dependencies
  alias_method :require_or_load_without_multiple, :require_or_load
  def require_or_load(file_name, const_path = nil)
    if file_name.starts_with?(Rails.root.to_s + '/app')
      relative_name = file_name.gsub(Rails.root.to_s, '')
      #@engine_paths ||= Rails::Application.railties.engines.collect{|engine| engine.config.root.to_s }
      #EDIT: above line gives deprecation notice in Rails 3 (although it works in Rails 2), causing error in test env.  Change to:
      @engine_paths ||= YourAppName::Application.railties.engines.collect{|engine| engine.config.root.to_s }
      @engine_paths.each do |path|
        engine_file = File.join(path, relative_name)
        require_or_load_without_multiple(engine_file, const_path) if File.file?(engine_file)
      end
    end
    require_or_load_without_multiple(file_name, const_path)
  end
end

For a while this didn't work raising

TypeError in PostsController#index

superclass mismatch for class PostsController

but that was due to a mistyped class definition class PostsController < ActionController::Base which should be class PostsController < ApplicationController

Method 2

If you do not want to do this for all engine controllers etc., you can load the engine's controller before the definition in the main app

require PostsEngine::Engine.config.root + 'app' + 'controllers' + 'posts_controller'

class PostsController < ApplicationController
  # extended methods
end
johnmcaliley
  • 10,779
  • 1
  • 39
  • 47
Andrei
  • 9,162
  • 7
  • 68
  • 103
  • Andrei, is method 1 working for you in the test environment? I get method missing for 'require_or_load'. Works fine in dev – johnmcaliley Mar 02 '11 at 13:32
  • 1
    after closer inspection, it is a method missing for "call" on :stdout:Symbol. This is coming from the "config.config.active_support.deprecation = :stdout" in "config/environments/test.rb". Line #8 in your example 1, gives a deprecation notice. It should be changed to "@engine_paths ||= YourAppName::Application.railties...." Works in test after that – johnmcaliley Mar 02 '11 at 14:01
2

I've created a gem based on the code from Andrius and Andrei above. Instead of copying around that code, just require the mixable_engines gem. Only works with rails 3 right now.

https://github.com/asee/mixable_engines

https://rubygems.org/gems/mixable_engines

@Andrei and @Artrius: I've credited you in the license file, let me know if you want your real name or some other credit.

Tim Morton
  • 21
  • 1
1

You can use Ruby's send() method to inject your code into the controller at the time the engine is created...

# lib/cool_engine/engine.rb

module CoolEngine
  class Engine < ::Rails::Engine

    isolate_namespace CoolEngine

    initializer "cool_engine.load_helpers" do |app|
      # You can inject magic into all your controllers...
      ActionController::Base.send :include, CoolEngine::ActionControllerExtensions
      ActionController::Base.send :include, CoolEngine::FooBar

      # ...or add special sauce to models...
      ActiveRecord::Base.send :include, CoolEngine::ActiveRecordExtensions
      ActiveRecord::Base.send :include, CoolEngine::MoreStuff

      # ...even provide a base set of helpers
      ApplicationHelper.send :include, CoolEngine::Helpers
    end
  end
end

This method spares you from having to redefine the controller inheritance within your main app.

Frank Koehl
  • 2,852
  • 2
  • 26
  • 34
1

If you do not want patch active support to change the load order as suggested in Rails engines extending functionality, you can make use of a rack middleware for authentication. If authentication is done as part of every controller action, this approach might save you lot of code and time.

Community
  • 1
  • 1
Deepak N
  • 2,501
  • 2
  • 29
  • 44
  • this is not only about authentication. I should not mention it... I am fine with patching ActiveSupport - just want to know where I need apply that code. – Andrei Feb 18 '11 at 19:35
0

@cowboycoded method 2 in conjunction with require_dependency and config.reload_plugins worked for me on Rails 3.2.2 / Ruby 1.9.

Here is the code: https://stackoverflow.com/a/9790497/22237

Community
  • 1
  • 1
Kris
  • 16,882
  • 6
  • 79
  • 99