The Semi-Isolated Rails Engine

(All of this is accurate, so far as I know, in Rails 3.2.3. If you are reading this later in future Rails versions, mileage may vary).

Rails 3 introduced plugins-as-gems, and the special case of Engines. An Engine is basically  a library of code that can define it’s own views, controllers, models, assets, etc, in it’s own codebase, that will be available for the Rails app. (An Engine doesn’t actually need to be defined in it’s own gem, it can be defined anywhere that ends up in the load path. but it’s own gem is typical).  You can have Rails generate a skeleton for an Engine plugin as gem, with `rails plugin new enginename --full`.  (Without the –full, it’d be a less powerful plugin without full Engine features — actually it ends up being pretty much just an ordinary gem).

A “plain” engine (as opposed to ‘isolated’ engine we’ll discuss later) basically “inserts” controllers, views, and models into the host app — they’re added to the load paths to part of the host app same as any locally defined controller, view or model.

Additionally, routes defined in your $engine/config/routes.rb will be automatically included in the host app. I’m not sure if they’ll be included before or after host app routes; route definition order matters in Rails3 routing.

Name collisions?

If there’s a name collision, the thing with the same name in the host app will usually ‘win’, and the one in the gem will be in accessible to most code (in gem or in host app).  If there’s name collision between two gems, it probably depends on load order (what order they’re referenced in the Gemfile, usually).

This is pretty much what you’d expect to happen, so long as the host app version really wins, I think it’s “right”.  (With helpers specifically, things can sometimes get confusing and not behave how you expect. I now can’t find the message I think I sent to the rails-user listserv on this at some point, and maybe it’s been changed/fixed in recent versions of rails.)

You can put your models, views, and controllers in module namespaces just exactly the same as you can if you were adding em to any Rails app, in order to try and prevent namespace collision. They’ll work just exactly the same way — the point of an Engine is the stuff in an engine is in the host apps load paths just the same as if it was really in the host app source locations.

Avoiding routing name collisions can be handled the same way, in a ‘plain’ engine, using the Rails3 router :namespace function, or any of the other related router functions (:as, :module, :path, etc.)

Some Engines handle routing by not including routes in $engine/config/routes.rb, where they’ll be automatically loaded by Rails, but instead loading routes into the host app using their own logic, so it can be done just so. This is especially useful for routes that should be changed by host app configuration. For instance, Devise and it’s `devise_for` method that the host app calls manually in it’s own routes.rb.

Isolated Engines: Rails 3.1

Rails 3.1 introduced the “isolate_namespace” directive, which you can add to your engine module.

The one main effect this has is actually on routing. $engine/config/routes.rb are not added to the host app’s routing.  Instead, Rails creates a little Rack mini-app out of your engine (or maybe any Engine already is this?), with your engine’s routing in it, so that host app can mount the Engine into the host app’s own routing, using the standard Rails routing ‘mount’ directive for Rack apps. See the Engines guide (or the edgeguide version, with slightly expanded information).

It also makes the engine’s $config/routes.rb behave a bit differnet as far as default routing params, assuming all routes are :namespace’d, making sure  the routing helper methods are available to your Engine’s controllers and helpers (and at the right method names), etc.

On top of this, it changes how rails generators work inside your engine. You can use rails generators inside an engine to add controllers and models. In a ‘plain’ engine, if you call `rails generate controller foo`, it’ll add an $engine/controllers/foo_controller.rb, just like any rails app.  It’ll add an `$engine/views/foo` directory and an `$engine/helpers/foo_helper.rb`. Just like an app.

In an Engine with `isolate_namespace`, if you call `rails generate controller foo`, it’ll namespace everything it generates for you:  `$engine/controllers/$enginename/foo_controller.rb` will contain a controller whose class is EngineName::Foo.  Similarly, view folder in `$engine/controllers/$enginename/foo`, etc.

Isolated engines are convenient for many cases.  You can have Rails generate a new skeleton for an isolated engine with `rails plugin new enginename --full --mountable`

There’s one aspect of them, though, that you may or may not want — and is fortunately pretty easy to change, giving you what I’ll call a Semi-Isolated Engine.

More Isolation Than you Might Want: Controller inheritance

There’s one aspect of isolated engines that ends up being a bit confusing — It’s actually not caused by the `isolate_namespace` directive in the Engine, but purely by the Rails generators — in fact, purely by the `--mountable` arg to `rails plugin new engine_name --full --mountable`.

Let’s look at how controller inheritance works.

If you use the `rails generator controller` to generate in your engine, if you look at it you’ll see that it’s defined as < ApplicationController — inheriting from the class called ApplicationController — just like a controller in a normal app. But your engine gem doesn’t have an ApplicationController (at least it ought not to, at least not a top-level-namespace ::ApplicationController) — what’s it inheriting?  Well, it’s inheriting from the ApplicationController in whatever host app it happens to be running in.

This means common logic in the host apps ApplicationController is available to engine controllers. (Say, a current_user? method; the engine would obviously need to document it’s conventions).  It also means all the helper methods loaded into the host app in a way that they apply to all controllers, will be available to engine controllers/views.  It also means that, by default, the default rails template layout for controllers in the engine is the host app’s `application` layout — or any other default layout specified in host app ApplicationController.

Sometimes that’s all actually nice, but sometimes you want more isolation. If you generate an engine with `rails plugin x --full --mountable`, you get it.  But how you get it is a bit confusing at first.

mountable/isolated generation of Engine::ApplicationController

If you generate a `mountable` (ie, isolated) engine, and then you use `rails g controller` to generate a controller, you’ll see it’s still defined as `< ApplicationController`. And yet it doesn’t actually inherit the behavior of the host app ApplicationController — it’s got no logic from host app ApplicationController, no helpers, won’t find it’s layout, etc.

What’s going on? It’s a different ApplicationController.  When you generate an engine with rails –full –mountable, it generates an EngineName::ApplicationController to $engine/controllers/$engine_name/application_controller.rb.

Because of the way Rails constant lookup works, it’s finding this ApplicationController.

And it generated a layout in your engine too at $engine/views/layouts/$engine_name/application.html.erb.

That’s the layout used by all your engine controllers, by default too.

multiple ApplicationController’s, really?

While this level of isolation is perhaps useful for many (most?) Engines, I question the decision to ‘override’ the ApplicationController class name and count on ruby constant-lookup in namespaces to get to the right one. ruby namespaced constant lookup is notoriously confusing, and changes from ruby version to version not always in documented ways.  I think it’s just asking for developer confusion and bugs.

Fortunately, it’s only a feature of the Rails generators (both the ‘rails plugin new‘ and `rails generate controller` within an isolated_namespace engine). Got nothing to do with actual rails runtime logic.

If you want to do it differently, no problem.  Go change $engine/controllers/application_controller.rb to, say, engine_name_controller.rb instead, and the layout to engine_name.html.erb.  All of your engine controllers should now “< EngineNameController” instead of “< ApplicationController“.

You’ve got the exact same behavior, just with less confusing and error prone names.

Sadly, `rails g controller` in an isolated_namespace engine will still generate< ApplicationController“, you’ll have to manually change it each time you use the generator.

Now, for the Semi-Isolated Engine

Okay, now we can get to the actual point. While isolating controllers like this can be useful sometimes, sometimes it’s not. You might still want the routing isolation that “isolate_namespace” gives you, and the convenient change in behavior of the rails generators under that condition.

But you do want your engine controllers to inherit from the host app ApplicationController. No problem!  Just change that engine ‘main’ controller to “< ApplicationController”. You could do that even without the name change we discussed above, by properly scoping to top-level namespace, but that would lead to the confusing (but correct!) EngineName::ApplicationController < ::ApplicationController.

Less confusing if we changed the name as recommended above, say if your engine is the Widgetizer, Widgetizer::WidgetizerController < ApplicationController.

Now,

  • any logic in the host app ApplicationController is available in engine controllers.
  • Your engine controllers are by default using your engine’s ‘main’ layout instead of the host app’s — just delete the engine layout and they’re by default using the host app’s, that’s it!  (Delete $engine/app/views/layouts/widgetizer/application.html.erb, or $engine/app/views/layouts/widgetizer/widitizer.html.erb if you changed the names as recommended).
  • If you have logic which you do want available to all engine controllers but shouldn’t be in teh host app, just add it to your intermediary engine main controller, right? Because SomeEngineController < EngineController < ApplicationController. (With Rails 3.2+ hierarhical view lookup, all views can be looked up through this chain, not just layouts).
  • Because of isolate_namespace, the host app is still not automatically given the helpers in the engine — great! (If you want to manually expose engine helpers in the host app, see advice in the Engine Guide).
  • Helpers in the engine are a bit more confusing. Since engine controllers subclass the host app ApplicationController, helpers from the host app areavailable in engine controllers. In some cases this is useful, in most others it probably won’t cause a problem.
    • If there is name collision between helper methods in host app and engine, when called from within an engine controller, the engine helper method ‘wins’. Which is great. (The engine helper can even call ‘super’ to get access to the host app version, although there are few cases where an engine helper could rely on ‘super’ existing.) However, this is reliant on details of how and in what order Rails include’s helper modules into controllers, something that’s changed in past rails versions, I’d be a bit cautious of relying on this continuing to work, sadly.

So there you have it, the “Semi-Isolated Rails Engine”, a design that works well for me for certain kinds of engines. It’s a testament to Rails 3.x nice, clean, flexible, consistent, well-designed architecture that we don’t need to fight with Rails actual runtime logic at all to do this, we don’t even need to change it, we just need to make different choices than the Rails engine generators make. If someone wanted to, they could even make their own generators that behaved this way for a ‘semi-isolated rails engine’.

This entry was posted in General. Bookmark the permalink.

12 Responses to The Semi-Isolated Rails Engine

  1. Chris says:

    Djaïsus ! More than useful post ! Thanks

  2. C Cole says:

    Having trouble implementing your solution in my semi-isolated engine. Now have an engine application_controller that begins with “class ApplicationController < ApplicationController", but it seems the methods aren't available in my main app.

  3. jrochkind says:

    I’m not sure what you’re doing is what I suggested. What are you trying to do, and how?

    “class ApplicationController < ApplicationController" doesn't make any sense. Perhaps you want "class ApplicationController < ::ApplicationController" (note the double colon for top-level namespace). Although that's still confusing and not what I recommend (or what I recommended in the post above).

    But even if that works, it will NOT make methods provided in your engine's top-level controller automatically included in every controller of your main app. If I implied it would, I wrote confusingly.

    What are you actually trying to do, what's your goal? But recommend maybe asking the question on stackoverflow, I may not be able to help you figure it out, sorry!

    If you want to provide methods from an engine that will be available in all controllers in the main app, the most straightforward way is simply to provide a module with those methods, and have the main app manually include it in it's ApplicationController: `include MyEngine::ControllerMethodsForMainApp`

  4. C Cole says:

    Sorry for the confusion. Your discussion about ‘multiple ApplicationController’s, really?” I thought was addressing the need to have engine application_controller methods inherited into a main app. Having other controllers ‘SomeController < ApplicationController' works just fine, since there is no name collision, but that seems to be the problem with engine's ApplicationController. I agree, the 'ApplicationController < ApplicationController' makes no sense–I was getting desperate after many hours of trying to solve this. I don't want to have to put 'Include' within the main app–I want to have it seamlessly integrated.

  5. jrochkind says:

    Yeah, this stuff is confusing to talk about.

    What I wanted to do was have _main app application controller_ available in my engine code. You want the reverse, to have certain engine-provided methods available in the main app?

    I think it’s fine to simply do this manually with a module and instructions to include in main app — I don’t think there’s a better way to do that, and that way strikes me as just fine.

  6. nandogs says:

    Nice post, now I see the light! I’m having this issue with third-party engines (like tolk, for example). I guess the only way to go in these cases is forking the engine, isn’t it?

    Thanks a lot for the info!!!

  7. SC says:

    FYI: at least in Rails 3.2.8, –mountable implies –full

  8. SC says:

    I take that back. There are definitely differences. Depending on whether you specify “full”

  9. nezek says:

    What about inheriting named routes?
    Because the namespace is isolated, when I use any named route of the main app, it calls url_for, which results in, for example, this error:
    “ActionController::RoutingError: No route matches {:action=>”new”, :controller=>”sessions”}
    […] /gems/actionpack-3.2.8/lib/action_dispatch/routing/route_set.rb:532:in `raise_routing_error'”

    Since my SessionsController is outside the isolated namespace, any named helpers will pass the wrong arguments to url_for–it would need to use :controller => “/sessions”. Any idea how I can work around this? I can’t just preface the named routes with “main_app” because some of them are in filters in modules included from installed gems.

  10. jrochkind says:

    Lately, I just plain don’t use isolated namespaces. I think you don’t want to unless you really want virtually no interaction between the host app and the gem; it makes things too confusing, in my opinion. I use plain old non-isolated engines, and understand how it all works.

  11. Pingback: Michael Chase Pell's Insights | Exploring The Difference Between Rails Engines

  12. reagleton says:

    @nezek I was running into the exact same problem as you but I just found the following answer on Stackflow – http://stackoverflow.com/questions/10712074/missing-devise-routes-helpers-inside-of-rails-engine-views.

    The second answer describes the addition of a helper that will automatically add “main_app” if it can’t find the path normally. This worked for me where my layout (inherited from my application) contains many route helpers for the navigation.

    This, plus the semi-isolated engine described above, seems to have to given me virtually full access for my engine in my app.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s