Extending controllers of a Rails 3 Engine in the main app
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
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
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.
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
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.