Engines in Ruby on Rails provide a great way to embed reusable functionality in an application by modularizing a complete MVC stack. Basic engine functionality recently moved into the Rails core, but up to now there has been little in the way of guidance on how (and when) to create an engine in Rails 2.3. To address this need, this article-- the first of two parts-- presents tips and techniques for creating, integrating, and extending engines in Ruby on Rails.

When to Use Engines

Engines were originally conceived as an alternative to the familiar plugin-with-generator approach to code reuse. Rather than altering or inserting code into the host application with a generator, engines provide an alternate set of models, views, and controllers that run inside your application, leaving the host application largely untouched. As such, they're easier to keep up-to-date and much easier to extend.


This makes the engine approach a good candidate in many situations where you have core functionality that is largely the same between applications. For example, if you have an application that you offer as an ASP with the potential for client customization, you may find it easier to move your core functionality into an engine and let the host app extend it to implement client-specific business rules.

Another potential use of engines is abstracting a common set of code that you plan to use across multiple applications. For example, our applications almost always incorporate some form of authentication and user management. There are slight variations between applications, but the basic creation, administration, and authentication of users is pretty much the same across the board.

Cutting and pasting is one of the cardinal sins of software development. Before we started using engines to encapsulate core functionality, we had to copy the models, views, controllers and migrations pertaining to user management from an existing application at the start of any new project. And if we made any enhancements to user management in one application, we had to manually migrate the changes to all of our other apps. Moving to an engine provides a mechanism for sharing this common functionality without sacrificing maintainability.

Creating FrontDesk: A User Management Engine

The rest of this article will walk you through the process of creating a user authentication and management engine and extending it from the host application.

Let's say that we have an existing application that is used to manage widgets. Up to now we've allowed anyone to view and edit widgets without having to log in, but now we're ready to impose some authentication and authorization requirements on widget management.

We'll encapsulate core authentication and user management functionality in a reusable engine called FrontDesk. Code reuse is a key concern, because we anticipate having other applications that will require similar functionality; therefore anything specific to the host widget management application will be accommodated by extending FrontDesk as needed.

Generating Your Engine

Owing to its origins as a plugin-on-steroids, the first step in creating an engine involves the plugin generator:

$ script/plugin generate front_desk

  create vendor/plugins/front_desk/lib
  create vendor/plugins/front_desk/tasks
  create vendor/plugins/front_desk/test
  create vendor/plugins/front_desk/README
  create vendor/plugins/front_desk/MIT-LICENSE
  create vendor/plugins/front_desk/Rakefile
  create vendor/plugins/front_desk/init.rb
  create vendor/plugins/front_desk/install.rb
  create vendor/plugins/front_desk/uninstall.rb
  create vendor/plugins/front_desk/lib/front_desk.rb
  create vendor/plugins/front_desk/tasks/front_desk_tasks.rake
  create vendor/plugins/front_desk/test/front_desk_test.rb
  create vendor/plugins/front_desk/test/test_helper.rb

As you can see, this command creates the standard set of plugin files in /vendor/plugins/front_desk. But to turn this into an engine, we'll need some additional files and folders:

$ mkdir vendor/plugins/front_desk/app
$ mkdir vendor/plugins/front_desk/app/controllers
$ mkdir vendor/plugins/front_desk/app/models
$ mkdir vendor/plugins/front_desk/app/views
$ mkdir vendor/plugins/front_desk/config
$ touch vendor/plugins/front_desk/config/routes.rb
$ mkdir vendor/plugins/front_desk/db
$ mkdir vendor/plugins/front_desk/db/migrate
$ mkdir vendor/plugins/front_desk/public

(Someone should write an engine generator, shouldn't they?)

That's enough to get started. Now let's dive in and start adding some code.

Creating the Models, Controllers, Views and Migrations

FrontDesk will use the popular authlogic gem to handle user authentication, so follow the gem's README to create the user and user_session objects and their corresponding views and controllers. If you use scaffolding to create models, controllers, views, and migrations, you will need to manually move them from the host application to vendor/plugins/front_desk/.

After you're done, the front_desk folder should look like this:

The FrontDesk folder structure

What You Need to Know About Migrations

Rails doesn't support migrations from plugins, so although we create our migrations inside the FrontDesk folder, we need to create a rake task to move the files to the main /db/migrate. (Thanks to Ryan Bates' RailsCast on engines for this tip.) Note that the same rake task can be extended to move over any other files that your engine may require, including style sheets and graphical assets for the /public directory.

Inside of the engine's tasks/ directory, you'll find a stub front_desk_tasks.rake file that Rails created for us. Let's add our installation code here:

namespace :front_desk do
  desc "Install migrations and other files from FrontDesk."
  task :install do
    system "rsync -ruv vendor/plugins/front_desk/db/migrate db"
    system "rsync -ruv vendor/plugins/front_desk/public ."
    puts "FrontDesk file sync complete. Next steps:"
    puts
    puts "  - Run rake db:migrate to complete the installation."
    puts "  - Add the following lines to config/environment.rb:"
    puts "        config.gem 'aasm'"
    puts "        config.gem 'authlogic'"
    puts "        config.gem 'cancan'"
    puts
  end
end

Now, when you run rake front_desk:install, everything from the migration and public folders in the FrontDesk engine will be moved over to the host application. Be sure to migrate your database and follow the instructions for altering environment.rb after the installation.

Routing

Next, let's take care of the routes. We need all of the FrontDesk-related routes to live in /vendor/plugins/front_desk/config/routes.rb:

ActionController::Routing::Routes.draw do |map|
  map.login 'login', :controller => 'user_sessions', :action => 'new'
  map.logout 'logout', :controller => 'user_sessions', :action => 'destroy'
  map.resources :users
  map.resource :user_session
end

The host application will automatically map the routes from the engine. Note that any routes with conflicting names in the main config/routes.rb will override those specified in the engine.

Extending the Host Application

Our authentication and authorization functionality requires that certain methods be available to our controllers, so we need to find a way to include them with every controller in the host application. We'll do this by creating a submodule in FrontDesk.

Inside of front_desk/lib/front_desk.rb, we will define a module containing controller extensions. After adding the methods that we need for interacting with authlogic, the file should end up looking like this:

module FrontDesk

  module ControllerExtensions

    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.user
    end
  
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end
  
  end

end

(Note that if you're going to have a lot of different modules inside of your engine, you can keep the code segregated by creating a subfolder in lib with the same name as your engine, e.g. lib/front_desk/. As long as lib/front_desk.rb defines the FrontDesk module, anything inside of the lib/front_desk/ directory will be appropriately namespaced.)

Now we need to let the host application's controller know about this new functionality. Unfortunately, a simple include inside /app/controllers/application_controller.rb won't do the trick; we'll run into namespacing issues. Instead, add the following to the init.rb file inside of the FrontDesk folder:

ActionController::Base.send :include, FrontDesk::ControllerExtensions

This will automatically include the methods we define inside of the ControllerExtensions module in the host application's controller, and all controllers that inherit from it.

Tips for the Development Environment

By default, Rails will only load engines and plugins once, during application startup. While this is ideal for the production environment, in development this means that any time you make a change to your engine's code, you'll have to start and stop your application. Luckily, there's a way around this. Add the following line to your application's config/environment.rb file:

config.reload_plugins = true if RAILS_ENV == 'development'

After making this change, you'll start experiencing an intermittent error when you request pages served up by the engine: "A copy of FrontDesk::ControllerExtensions has been removed from the module tree but is still active!" This is easily remedied by ensuring that all of the engine's code is available when it's reloaded. Add the following to the top of front_desk/init.rb:

require 'front_desk'

Integration with the Host Application

The host application is now ready to go, with user authentication and creation functionality being delivered by FrontDesk. You can test that the engine is working by accessing login/ at your localhost:

Sign in page

To Be Continued...

In the next installment of this article, I'll walk you through the process of extending and overriding models, controllers, and views from the host application. Stay tuned!

Comments

Arvind
Arvind
09/19/2010
Great article! I'm trying to embed the "Savage Beast" plugin into my Rails app using Engines. The problem I'm facing is ... In the "lib/savage_beast/authentication_system.rb" file of the plugin are defined a bunch of methods accessed in the views of the plugin. I included the particular module in my application controller (rails 2.0.2) application.rb using - include SavageBeast::AuthenticationSystem and then I went ahead and overrode a number of methods in it. The server never picks up the overridden methods and always goes for the ones still in the plugin. Could you shed some light on what I might be doing wrong and how could I correct it? Thanks!
Dom
Dom
03/08/2011
This is an excellent article, but whatever happened to part two? I was really looking forward to the section on extending and overriding models in an engine. Is there someone else I can find this information that you know of.
Leave a Comment