Introduction
In Rails world it’s just a piece of code, which plugged to existing application, extends its basic functionality. Good examples would be: ActiveAdmin, AmberBit’s Translator or Devise. Also Rails application is example of an engine.
Really nice use for engine that comes to my mind would be API versioning. A website would have no API at all on start. One instance of it would be deployed with /api/v1
mounted, and other /api/v2/
. Clean, easily separable and providing only functionality that is needed in given case.
There is also detailed guide available on RailsGuides.
Getting started
All you really need is to create new directory for your engine, let’s say foo_baz
, .gemspec
file and a engine.rb
file within lib/foo_baz
directory. The key thing here is to inherit from ::Rails::Engine
.
module FooBaz
class Engine < ::Rails::Engine
isolate_namespace FooBaz
end
end
And to mount it into existing Rails application, add newly created library to Gemfile
:
...
gem 'foo_baz', path: 'lib/engines/foo_baz'
...
Nothing unusual in foo_baz.gemspec
file as well. Very basic, minimal content would be for example:
$:.push File.expand_path("../lib", __FILE__)
Gem::Specification.new do |s|
s.name = "foo_baz"
s.version = "0.0.1"
s.authors = ["Kamil Dziemianowicz"]
s.summary = "Summary of FooBaz."
s.files = Dir["{app,config,db,lib}/**/*"]
s.add_dependency "rails", "~> 4.2.4"
end
Of course fully-featured engine will be much more complex. Rails has built-in generator which will create dummy structure & files for your engine and even more. To run it, simply invoke rails plugin new foo_bar
with option --mountable
. It will generate directory structure not surprisingly similar to standar Rails application structure.
create
create README.rdoc
create Rakefile
create foo_bar.gemspec
create MIT-LICENSE
create .gitignore
create Gemfile
create app
create app/controllers/foo_bar/application_controller.rb
create app/helpers/foo_bar/application_helper.rb
create app/mailers
create app/models
create app/views/layouts/foo_bar/application.html.erb
create app/assets/images/foo_bar
create app/assets/images/foo_bar/.keep
create config/routes.rb
create lib/foo_bar.rb
create lib/tasks/foo_bar_tasks.rake
create lib/foo_bar/version.rb
create lib/foo_bar/engine.rb
create app/assets/stylesheets/foo_bar/application.css
create app/assets/javascripts/foo_bar/application.js
create bin
create bin/rails
create test/test_helper.rb
create test/foo_bar_test.rb
append Rakefile
create test/integration/navigation_test.rb
Most important parts of it are:
foo_bar.gemspec
- has the same function as you would expect from gem’s .gemspec filelib/foo_bar.rb
- FooBar module definitionlib/foo_bar/engine.rb
- mentioned before Engine fileconfig/routes.rb
- defining routes engine will provide, same as Rails app
Also noticable things are similar to Rails application application_controller.rb
and application_helper.rb
, both namespaced into FooBar
module. There is folder for image assets. Analogously, JavaScript and CSS files would go to app/assets/javascripts/foo_bar/*
and app/assets/stylesheets/foo_bar/*
folders. Views shoud be put into app/views/foo_bar/*
folder and so on… The rule of thumb is that everything should be namespaced, and if it’s namespaced, it should be put where it usually goes in Rails app, but including foo_bar
subfolder.
Isolate namespace
Very important part of engine class definition is declaration of namespaces isolation:
...
isolate_namespace FooBar
...
It will force scoping all classes (AR models, controllers, helpers etc…) into FooBar
module. Thereby both base application and engine library could define User
class, with corresponding database table, separated views, and they won’t clash. Of course it is not mandatory to isolate engine from base application. It could be useful when created engine won’t be used by other people, or developer is aware of all classes and routes defined in it. Generally, better keep them namespaced.
Rails generator
Created engine comes with executable bin/rails
script. Its main purpose is to help generate proper models/views/controllers. Keep in mind, that all engine resources should be namespaces inside engine module name. To run sample generator:
$ bin/rails g model kitty name:string
In result, proper folders structure and class-module hierarchy will be preserved:
...
invoke active_record
create app/models/foo_bar/kitty.rb
...
And content of Kitty model file:
module FooBar
class Kitty < ActiveRecord::Base
end
end
Tie things up
Let’s start with routes. Most likely you’d like your engine to add new pages with own paths to base app. To achieve this, simply amend routes.rb
file with new definitions:
FooBar::Engine.routes.draw do
resources :kitties
end
In order to activate those routings in base application, it’s routes.rb
file also needs to be amended as follows:
Rails.application.routes.draw do
...
mount FooBar::Engine, at: "/foo_bar"
...
end
To see it actually works, let’s check all available routes with $ rake routes
command:
Prefix Verb URI Pattern Controller#Action
foo_bar /foo_bar FooBar::Engine
root GET / pages#home
Routes for FooBar::Engine:
kitties GET /kitties(.:format) foo_bar/kitties#index
POST /kitties(.:format) foo_bar/kitties#create
new_kitty GET /kitties/new(.:format) foo_bar/kitties#new
edit_kitty GET /kitties/:id/edit(.:format) foo_bar/kitties#edit
kitty GET /kitties/:id(.:format) foo_bar/kitties#show
PATCH /kitties/:id(.:format) foo_bar/kitties#update
PUT /kitties/:id(.:format) foo_bar/kitties#update
DELETE /kitties/:id(.:format) foo_bar/kitties#destroy
Notice how engine path is mounted under defined /foo_bar
part. This allows having the same resources names in both main Rails application and mounted engine. The tricky part about routes is accessing base application routing helpers in engine and vice versa. Imagine engine view, where we want to include link to root path /
. Usually you would just write:
<%= link_to "Home", root_path %>
But you’ll end up with familiar screen and error message
Showing /home/kaa/amberbit/blog/engine/foo_bar/app/views/foo_bar/kitties/index.html.erb where line #1 raised:
undefined local variable or method `root_path' for #<#<Class:0x000000048abed0>:0x000000048ab318>
Why is that? Simply because FooBar engine doesn’t define own root path, and don’t know about base application routing. To access it, all helpers must prefix helpers with main_app
:
<%= link_to "Home", main_app.root_path %>
When using engine paths helpers from engine itself, you don’t need to prefix it. And analogously, don’t need to prefix base app routes in app itself, but that’s kinda obvious.
And how can I reference engine routes from main app? Using foo_bar
prefix of course!
<%= link_to "Kitties", foo_bar.kitties_path %>
If engine is not namespaced, its routing proxy prefix would be foo_bar_engine
instead of foo_bar
.
Tip for curious ones: main_app
and foo_bar
are ActionDispatch::Routing::RoutesProxy instances.
When dealing with many engines, it sometimes could be troublesome to mount them all in app routes. To ease this task, engine could actually mount itself. But keep in mind that this fact will be hidden from developer at first glance:
module FooBar
class Engine < Rails::Engine
isolate_namespace FooBar
initializer "foo_bar", before: :load_config_initializers do |app|
Rails.application.routes.append do
mount FooBar::Engine, at: "/foo_bar"
end
end
end
end
We used initializer to hook up into configuration before application runs.
Controllers inside engine
There is nothing magical happening in engine’s controller. The only thing to remember is to wrap them into correct module (FooBar in this example), and following hierarchy pattern put it in app/controllers/foo_bar/
subdirectory. Created views should go to app/views/foo_bar/[resource]/
subdirectory.
Generated engine comes with own ApplicationController
. Nice thing to do would be amending it to inherit from base application’s ApplicationController
instead of standard ActionController::Base
, so engine will have access to its helpers methods, trigger before_actions and inherit all other logic. To do that, just point to not scoped ApplicationController
:
module FooBar
class ApplicationController < ::ApplicationController
end
end
Another useful tip would be to re-use base application layout. As we have seen in first paragraph, generated engine comes with own layout file and assets manifests files. So when it’s possible to easily separate engine pages from base app by providing different layout, it’s often desirable not to do so. It’s just matter of declaring it correct way:
module FooBar
class ApplicationController < ::ApplicationController
layout "application"
end
end
But wait, the default for engine is also called application
. The significant difference is (again) namespacing, and originally it would be:
...
layout "foo_bar/application"
...
Defining models & database tables
Because it is possible to have the same class names in base application and inside engine, database tables needs to be somehow distinguished. Let’s take a look at model scaffolding again:
$ bin/rails g model kitty name:string
invoke active_record
create db/migrate/20151011103843_create_foo_bar_kitties.rb
create app/models/foo_bar/kitty.rb
invoke test_unit
create test/models/foo_bar/kitty_test.rb
create test/fixtures/foo_bar/kitties.yml
First hint is migration file name. And its content:
class CreateFooBarKitties < ActiveRecord::Migration
def change
create_table :foo_bar_kitties do |t|
t.string :name
t.timestamps null: false
end
end
end
Table name got prefixed with foo_bar_
. We could have expected that. While not so obvious, it’s also configurable option. To do that, simply edit lib/foo_bar.rb
module definition in following manner:
module FooBar
def self.table_name_prefix
"cute_"
end
end
Rails generator won’t pick that change though, and we would have to manually amend all create_table migrations to reflect this. Let’s see it in action. In Rails console:
> FooBar::Kitty.all
FooBar::Kitty Load (0.3ms) SELECT "cute_kitties".* FROM "cute_kitties"
SQLite3::SQLException: no such table: cute_kitties: SELECT "cute_kitties".* FROM "cute_kitties"
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such table: cute_kitties: SELECT "cute_kitties".* FROM "cute_kitties"
...
Other very important point to notice is that base application has no knowledge of engine migrations. Rails preferred way of dealing with it is to copy over migration files to db/migrate
. And there are even tasks helps doing just that:
$ rake foo_bar:install:migrations
Copied migration 20151011105543_create_foo_bar_kitties.foo_bar.rb from foo_bar
There is also other, more generic task, which will copy all not-yet-present migrations from mounted engines:
$ rake railties:install:migrations
You may sometimes forget to do it, and then pondering why your application is throwing exceptions. Luckily, there is a walkaround involving initialized tuning we’ve already seen in one of previous paragraphs. Just put following code in engine.rb
file:
module FooBar
class Engine < Rails::Engine
...
initializer "foo_bar", before: :load_config_initializers do |app|
...
config.paths["db/migrate"].expanded.each do |expanded_path|
Rails.application.config.paths["db/migrate"] << expanded_path
end
end
end
end
It will push all engine migration files into base application migrations list. As simple as that. And running migrate will also check engines for pending migrations:
$ rake db:migrate
== 20151011103843 CreateFooBarKitties: migrating ==============================
-- create_table(:foo_bar_kitties)
-> 0.0008s
== 20151011103843 CreateFooBarKitties: migrated (0.0008s) =====================
Loading files
What is loaded out of the box? Everything from engine you would expect from Rails application will be loaded on boot. That includes models, controllers, helpers, routes, locale files (config/locales/*
) and tasks (lib/tasks/*
). But what about custom directories you would probably have, like app/services
, app/workers
for instance? They needs to be loaded manually. And a good place to do it automatically is, once again, engine.rb
. I prefer splitting this task though. List of required files goes to lib/foo_bar.rb
module definition file:
module FooBar
...
def self.load_files
[
"app/workers/mailer_worker",
"app/services/kitty_washer"
]
end
end
and loading task to lib/foo_bar/engine.rb
:
initializer "foo_bar", after: :load_config_initializers do |app|
FooBar.load_files.each { |file|
require_relative File.join("../..", file)
}
end
Once again, app initializer is used.
Extending base application classes
Other thing your engine may want to do is to extend existing base application classes functionality. Quick & dirty way is to use Ruby class_eval
and instance_eval
functions. Given User
class being defined in application, you could do lots of interesting stuff, but that’s just regular Ruby. F.ex. create user.rb
file in app/models
directory with:
User.class_eval do
attr_accessor :name
has_many :kitties, class_name: FooBar::Kitty
User::MAX_KITTIES = Float::INFINITY
def cutest_kitty
kitties.sample
end
end
Also remember to declare class constants in module scope (User::MAX_KITTIES
), otherwise it will be declared as toplevel constant. I learned it hard way:
$ rails c
> User::MAX_KITTIES
(irb):2: warning: toplevel constant MAX_KITTIES referenced by User::MAX_KITTIES
=> Infinity
Of course, you could take more elegant approach and use ActiveSupport::Concern, but that’s topic for whole new article. Just remember to load such files manually (engine won’t do it as they live outside foo_bar
subdirectory).
Engine own executables
Adding executable scripts is not something entirely tied to an engine. Any gem library could do that, but I found it very useful and it’s worth mentioning. Add extra line to .gemspec
and any executable file placed in engine bin/
directory will be available for base application. No need for manual includes this time.
Gem::Specification.new do |spec|
...
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
...
end
Similar applies to Rake files, anything put in engine’s lib/tasks
folder will be runnable from base application, and furthermore rake -T
will list those tasks. For start, engine generator creates one dummy .rake
file with name corresponding library name.
$ rake -T
...
rake some_task:buy_kitties # Buy only cute kitties
rake foo_bar:buy_kitties # Buy moar kitties
rake foo_bar:install:migrations # Copy migrations from foo_bar to application
...
Conclusion
Rails engine is a broad topic. Everything related to Rails application could be applied here as well. I tried to focus on some non-standard tricks I picked up when writing one. What do you think? Maybe you also have some good practises worth sharing? Any constructive criticism is welcome :)
Post by Kamil Dziemianowicz
Kamil, long-term employee of AmberBit (joined 2012), is a full stack developer (Ruby, JavaScript).