Sharing ApplicationController methods with an engine.

1,775 views
Skip to first unread message

Tim Uckun

unread,
Sep 3, 2011, 9:57:12 AM9/3/11
to rails-...@googlegroups.com
I decided to create a Rails engine to move a lot of commonly used
models, helpers, and even gems into a gem I can include in my rails
apps. Most things are working OK but I can't seem to make a shared
ApplicationController which can be re-opened by the app that us using
the gem.

I am also running into some issues with re-opening modules for some
reason so maybe the two issues are related. For example...

I add the xebec gem as a dependency to my gem/engine. xebec requires
that I put "helper Xebec::NavBarHelper" into my ApplicationController
so I create a file in the engine like this.

/app/controllers/application_controller
class ApplicationController < ActionController::Base
helper Xebec::NavBarHelper
end


When I do this nothing happens the controller code is not being
called. So I require the file explicitly in my gem but then all the
helpers in the application_helper of the parent app go away.

I think one way to solve this problem would be for the parent
ApplicationController to inherit from this one but I am wondering if
there a better way to deal with.

Pat Allan

unread,
Sep 3, 2011, 6:06:25 PM9/3/11
to rails-...@googlegroups.com
Hi Tim

I'm not super experienced with engines, but here's my thoughts:

* Don't put ApplicationController in your engine - I think that should only be located in the application itself. What you can do to get your code into the app is make Xebec::ApplicationController a subclass of ActionController::Base, and then your app's ApplicationController a subclass of Xebec::ApplicationController (as you've hinted at the end of your email). This means you can avoid reopening the class.

* Also, with that custom superclass, you can include the helpers you want there, and they'll be loaded for every controller subclassing from it.

* Don't manually require anything that a Rails app would normally load itself (especially anything in app/models, app/controllers and app/helpers). That goes for the models, controllers and helpers in your engine as well as what's in the Rails app.

Cheers

--
Pat

> --
> You received this message because you are subscribed to the Google Groups "Ruby or Rails Oceania" group.
> To post to this group, send email to rails-...@googlegroups.com.
> To unsubscribe from this group, send email to rails-oceani...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/rails-oceania?hl=en.
>

Tim Uckun

unread,
Sep 3, 2011, 7:42:39 PM9/3/11
to rails-...@googlegroups.com
>
> * Also, with that custom superclass, you can include the helpers you want there, and they'll be loaded for every controller subclassing from it.

I wanted to avoid creating superclasses (or even modules that you'd
have to load into your classes). Your email gives me one idea though.
I could add my code to the ActionController::Base. Although I am not
crazy about that idea I think it might work. I'll have to decide what
I find less objectionable :)

>
> * Don't manually require anything that a Rails app would normally load itself (especially anything in app/models, app/controllers and app/helpers). That goes for the models, controllers and helpers in your engine as well as what's in the Rails app.

I need to figure out what is going on with the gemfile and requires.
Nothing in the engine gemfile is loading up automatically and neither
are the dependencies specified in the engine.

Nicholas Faiz

unread,
Sep 3, 2011, 8:04:50 PM9/3/11
to rails-...@googlegroups.com
Hi Tim,

Engines don't monkey patch classes, they override them. So, for example, if you bundle a User model in your Engine, it will replace (not enhance/extend/patch) the User model in the hosting application.

So you should never include an ApplicationController in your engine (well, I can't think of a use case where it would make sense, really).

Also, it might be a good idea to use simplicity where possible. The engine API is there to bundle a mini-app inside of another app., not really to group utility methods. If there are methods you want to reuse in your controllers across projects, start by putting them into a module and include them on the controller (or make a nice DSL on the controller which can reference some of your behaviours).

Hope this helps. I haven't really gone into your thread in depth as its Sunday, but the point I made above leapt out at me.

Cheers,
Nicholas

Nicholas Faiz

unread,
Sep 3, 2011, 8:19:46 PM9/3/11
to rails-...@googlegroups.com
Sorry, that was badly put - it's actually the reverse scenario. If the hosting app includes a class or template that the engine has, the hosting app.'s version of it is used instead. Anyway, the application_controller shouldn't be bundled.

Hrm, Sunday,
Nicholas

Ryan Bigg

unread,
Sep 3, 2011, 8:19:36 PM9/3/11
to rails-...@googlegroups.com




On 04/09/2011, at 10:04, Nicholas Faiz <nichol...@gmail.com> wrote:

Hi Tim,

Engines don't monkey patch classes, they override them. So, for example, if you bundle a User model in your Engine, it will replace (not enhance/extend/patch) the User model in the hosting application.

No, this is backwards. The hosting application's user model will override the engine's model. This is why you can override devise's views in your app.

You should be including a module from your engine into ApplicationController if you want to add additional functionality to it.

--
You received this message because you are subscribed to the Google Groups "Ruby or Rails Oceania" group.
To view this discussion on the web visit https://groups.google.com/d/msg/rails-oceania/-/J_odCSgjOdgJ.

Pat Allan

unread,
Sep 3, 2011, 9:15:17 PM9/3/11
to rails-...@googlegroups.com
On 04/09/2011, at 9:42 AM, Tim Uckun wrote:

>>
>> * Also, with that custom superclass, you can include the helpers you want there, and they'll be loaded for every controller subclassing from it.
>
> I wanted to avoid creating superclasses (or even modules that you'd
> have to load into your classes). Your email gives me one idea though.
> I could add my code to the ActionController::Base. Although I am not
> crazy about that idea I think it might work. I'll have to decide what
> I find less objectionable :)

Sometimes a simple include is better for long-term maintainability than automatically (and invisibly) injecting into a superclass.

>> * Don't manually require anything that a Rails app would normally load itself (especially anything in app/models, app/controllers and app/helpers). That goes for the models, controllers and helpers in your engine as well as what's in the Rails app.
>
> I need to figure out what is going on with the gemfile and requires.
> Nothing in the engine gemfile is loading up automatically and neither
> are the dependencies specified in the engine.

Are you adding the autoload paths in your Engine subclass? e.g.:

class Xebec::Engine < Rails::Engine
paths['app/controllers'] << 'app/controllers'
paths['app/views'] << 'app/views'
end

--
Pat

Tim Uckun

unread,
Sep 3, 2011, 9:17:36 PM9/3/11
to rails-...@googlegroups.com
>
> No, this is backwards. The hosting application's user model will override
> the engine's model. This is why you can override devise's views in your app.
> You should be including a module from your engine into ApplicationController
> if you want to add additional functionality to it.
>


Mmm. I don't know if this is true in all cases. When I added the
ApplicationController into my engine it broke things in the app that
was using the engine because it seemed to override the apps
ApplicationController. Then again it was 2:00 AM in the morning on a
Sunday so maybe I was a bit loopy at that time.

I think the safest thing to do at this point is to put my libraries in
modules and then have the application include them.

On a related note I remember a couple of apps used
https://github.com/pivotal/desert as their plugin architecture and it
was rather nice. IIRC it mixed in the desert classes with yours.

Tim Uckun

unread,
Sep 3, 2011, 9:26:10 PM9/3/11
to rails-...@googlegroups.com
>
> Are you adding the autoload paths in your Engine subclass? e.g.:
>
>  class Xebec::Engine < Rails::Engine
>    paths['app/controllers'] << 'app/controllers'
>    paths['app/views']       << 'app/views'
>  end
>


First of all I should clarify that my engine is not called Xebec. I am
using the Xebec gem in my engine. Xebec is a gem to create menus in
your application. I was hoping that I could create menus (all of which
are helpers) in my engine and then have the parent apps make use of
those menus by calling <%= top_menu %> in their templates or views.

As for the gems here is the conundrum.

My gemspec says


s.add_dependency 'acts-as-taggable-on'
s.add_dependency 'pg'
s.add_dependency 'pg_search'
s.add_dependency 'simple_uuid'
s.add_dependency 'devise'
s.add_dependency 'rails_config'
s.add_dependency 'xebec'

The gemfile in the engine says


source "http://rubygems.org"
gemspec


So somewhere in my engine I have a call to simple_uuid but it doesn't
work because bundler did not include that gem (in the parent
application). It installed the gem but did not load it. As a result I
have to put this on top of my lib/engine_name.rb

require 'rubygems'
require 'acts-as-taggable-on'
require 'pg_search'
require 'pg'
require 'simple_uuid'
require 'devise'
require 'rails_config'
require 'xebec'

Ideally rails would process the Gemfile in the engine(s) and then the
Gemfile in the parent app but it doesn't seem to be doing that.

Pat Allan

unread,
Sep 3, 2011, 9:30:03 PM9/3/11
to rails-...@googlegroups.com
On 04/09/2011, at 11:26 AM, Tim Uckun wrote:

> First of all I should clarify that my engine is not called Xebec. I am
> using the Xebec gem in my engine. Xebec is a gem to create menus in
> your application. I was hoping that I could create menus (all of which
> are helpers) in my engine and then have the parent apps make use of
> those menus by calling <%= top_menu %> in their templates or views.

Okay - I just was using what I could guess for an example. Adapt as you need to.

> Ideally rails would process the Gemfile in the engine(s) and then the
> Gemfile in the parent app but it doesn't seem to be doing that.

Bundler is for packaging your gem, not for loading it when used elsewhere - so Rails isn't going to look at gems/engines it's using to see if they have a Gemfile. It's not guaranteed other gems would use Bundler to manage their dependencies. Manually requiring what you need in that engine is the way to go.

--
Pat

Nicholas Faiz

unread,
Sep 3, 2011, 10:34:24 PM9/3/11
to rails-...@googlegroups.com
I think there's a bit of code in the engine API that actually randomizes the load path order between 1:30 am and 3:30 am ...

Tim Uckun

unread,
Sep 3, 2011, 10:37:46 PM9/3/11
to rails-...@googlegroups.com
On Sun, Sep 4, 2011 at 2:34 PM, Nicholas Faiz <nichol...@gmail.com> wrote:
> I think there's a bit of code in the engine API that actually randomizes the
> load path order between 1:30 am and 3:30 am ...
>


I think there is a bit of code in the Universe that randomizes a bunch
of stuff between 1:30 and 3:30 AM.

Ryan Bigg

unread,
Sep 3, 2011, 11:43:40 PM9/3/11
to rails-...@googlegroups.com

On 04/09/2011, at 11:26, Tim Uckun <timu...@gmail.com> wrote:
> Ideally rails would process the Gemfile in the engine(s) and then the
> Gemfile in the parent app but it doesn't seem to be doing that.
>

No, this is never going to be the case. You must specify your dependencies in your engine's gemspec file which will be loaded by Bundler because your application is treating your engine as if it were a gem. Or at least, it should be doing that.

If you've NOT specified your engine as a GEM DEPENDENCY of your application then things will not be loaded as normal.

If you'd like a better explanation why I can give you one over IM later this afternoon or I could write a blog post? :)

Tim Uckun

unread,
Sep 4, 2011, 4:33:25 AM9/4/11
to rails-...@googlegroups.com


I am always eager to learn so if you have the inclination please feel
free to ping me I am @timuckun almost everywhere.

BTW I am specifying the engine as a gem in the gemfile

gem 'engine_name', :path => 'vendor/engines'

Ryan Bigg

unread,
Sep 4, 2011, 5:07:56 AM9/4/11
to rails-...@googlegroups.com
Actually it's probably better to post here so that others can see it too. Here goes.

First up, you should be specifying the path like:

gem 'your_engine', :path => "vendor/engines/your_engine"

As you're telling Bundler the exact location of your engine. You could have multiple engines.

An engine should be considered a miniature application. It can contain controllers, models, views, initializers, unicorns, files in the lib directory, assets, etc. just like a real application can. However, it should be treated like Yet Another Gem Dependency to your application.

The reason for this is that Bundler would have a great deal of difficulty (at least, I think it would) determining which would be the canonical Gemfile. Should the Rails 3.1.0 exact dependency that you have in your engine's Gemfile be used, or should the 3.0.6 dependency in the application? What about the "~> 3.0.8" dependency required by one of the gems that you use? This is why your engine should be a gem.

Making it a gem is simple. The files that "rails plugin new [name] --mountable" come out with contains a gemspec. This file and this file only should be used to specify the runtime dependencies of your engine. Why? Well...

In your application you will want to (well, you are wanting to) use an engine. Now, Rails must find out about this engine somehow, but how? The same way a Rails application would find out about any other gem, because there's a line in the Gemfile for it!

For forem (https://github.com/radar/forem) I recommend that people put this line in their Gemfile:

gem 'forem', :git => "git://github.com/radar/forem.git"

When this gem is specified then all the dependencies specified as runtime dependencies for the gem are added as dependencies for the application as well, and treated just as if they were specified right there in the Gemfile.

When you run `bundle install` it will do its thing and install it just as a gem. Then when your application is initialized, these lovely lines near the top of config/application.rb will execute:

if defined?(Bundler)
  # If you precompile assets before deploying to production, use this line
  Bundler.require *Rails.groups(:assets => %w(development test))
  # If you want your assets lazily compiled in production, use this line
  # Bundler.require(:default, :assets, Rails.env)
end

This will require all the gems in the "default" group (that is, all the gems not in any group, as well as all the gems in the "assets" group and finally all the gems in a group matching the current name of the environment for Rails. Boom.

What does this do for an engine? Well, Bundler treats it just like any other gem: it loads the file at lib/forem.rb (https://github.com/radar/forem/blob/master/lib/forem.rb). This file is the "entrypoint" into the engine. It should set up the base for the engine, but not the engine itself! That is what forem/engine.rb is for (https://github.com/radar/forem/blob/master/lib/forem/engine.rb). Maybe I am a little OCD about the separation of different things into different files, but this is, in my mind, how it should be done.

Now, the fun begins.

At this point in the initialization cycle, Rails has done its thing (i.e. Rails::Engine is loaded, as is Rails::Application), and so we can inherit from Rails::Engine as we please. You can see this happening in lib/forem/engine.rb if you look hard enough. 

BONUS SIDE TANGENT TIME:

The isolate_namespace method call here will, er, isolate the namespace. Of the engine. Yeah. That. No really. It will:

1. Namespace your model's tables (i.e. forem's posts table is called "forem_posts" and the model respects that without us having to tell it to)
2. Imply that all the controllers, models, helpers, unicorns and views for this engine are namespaced too, rather than living at the root of the app directory. 
3. "Shield" the routing helpers from the application and the engine, allowing the engine and application to have routing helpers named the same but do different things depending on the context. To use the main application's routing helpers in an engine you would call it like "main_app.root_path". To use an engine's in the application, "engine.root_path". Magic!

And some other things that I can't recall right now. Namespacing your engine is SUPER HANDY and has a 100% guaranteed success rate of me not face-palming when I attempt to use it. Please do it! Namespace your engine for the same reason people namespace their gem code already: so you do not pollute the environment!

BE GREEN!

END BONUS SIDE TANGENT TIME.

By inheriting from Rails::Engine, the engine will let Rails know it is there. It will add the app/controllers, app/models, app/helpers and app/views for the engine to the load paths *after* the similar locations of the application. 

This, my friend(s) is why you can override the engine's <whatever> inside your application without Rails saying "oh no you just didn't", shaking its head side to side and snapping its fingers. 

Now, this is great! You can override stuff in the engine that you don't like with stuff in the application that you like more. But what if you want to patch the application from the engine?

Well, you could call `class_eval` on ApplicationController and treat that block that you'll inevitably pass it as if it were inside the *real* ApplicationController itself. Some people would have you do this in `config/initializers` and I currently have bounties on their heads. Instead, I would recommend you put a file like this in `app/controllers/application_controller_decorator.rb` and have that define a module called `ApplicationControllerDecorator` which is then included into `ApplicationController` by using an initializer.

That idea was stolen from Spree, which covers how to do the same, but patching engine functionality vs application functionality, here: http://spreecommerce.com/documentation/customization.html#logic-customization. Same forest, different tree.

I can't come up with the code right now to do it, but I know it is possible as I have seen it done.

And that concludes this lengthy post on how engines work.

If you have any further questions I would be more than happy to answer them, just not in such a long form as this.

Until next,
Ryan Bigg
Reply all
Reply to author
Forward
0 new messages