(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.
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 `
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
Because of the way Rails constant lookup works, it’s finding this ApplicationController.
And it generated a layout in your engine too at
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 “
You’ve got the exact same behavior, just with less confusing and error prone names.
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.
- 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/widitizer.html.erbif 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’.