Rails 3 has been out quite a while in "rails" time. Back in the world of rails 2 most custom code was added via rails plugins stored in the vendor directory. Although this allowed for a clean dependency relationship when new versions of plugins came out it made it difficult to upgrade existing code bases. So when it came time to upgrade to rails 3 this contributed to a lot of people having difficultly upgrading their existing codebases. Along came the new revised rails 3 engine system which improved upon the existing railstie system. Although not perfect it gives a bit more structure and modularity for those wanting to create reusable rails plugins. In this article I will go through the following:
- basics of a rails 3 engine
- creating an engine from scratch
Basics of a Rails 3 engine
Realistically rails apps, engines, railties and plugins are extremely similar. You will find that many people will use the terms interchangeable. Although they shared most functionality their are minor difference between each of these. For the purposes we are going to focus on the newer rails engines. In simplest terms and engine can be either a full rails apps that are able to run on their own or when included in another app or they can be designed to function only when included in another app. The benefits of the former makes it much easier to test your engine outside of other code and dependencies. This keeps things modular and makes it much easier to make changes later. For the purposes of this article we will be building a full rails app that will also function as an engine.
At a very high level what engines essentially do is add additional files into your available ruby object space as well as adding additional paths for your load paths, intiializers, configs, etc that your main app will use. Additionally engines allow you to define additional middleware that can be added after varies hooks during the boot sequence. This could allow you to build an engine that acts as a middleware for your app so you could for example do some pre-processing before a request is processed by the main rails stack. You can read more about the specifics in the gist referenced below.
Creating an engine from scratch
Let's go through building a very simple blog engine from scratch. You won't need any special dependencies just your text editor/IDE of choice and rails 3 installed.
The first step is to simply create a new rails 3 project. I'll go through the basics for anyone new to the process:
$ rails new blog_engine
Now we can scaffold out a simple blog mvc structure:
$ rails g scaffold Blog author:string content:text
This will have created sample model, view and controller code along with the necessary database migrations and routes required to access your resource. You can configure your Gemfile and modify the database.yml to work with your database of choice. For simplicity let's just use the generated sqlite3 one. You will also need to remove the generated public/index.html to allow you to view your results thus far.
Starting the app up with
$ rails s
you should be able to see your new app running at http://localhost:3000. So far we have a simple blog app. Unfortunately we cannot easily load this into another app as an engine yet. Next let's add the files required to turn this app into a rails engine. First we start by adding the file required so rails understands this will be a rails engine (rather than a railstie or plugin). It can be as simple as:
or more complex like
So this should tell rails that this app can also function as an engine but it won't quite work yet. There are two issues. The first is that rails may not be loaded yet and the second being that we haven't yet told ruby to load this file. To achieve this create a file like below:
Great! So now our app has been defined as an engine and all of its paths, configs, initializes should be add to whichever rails app we add it to. There are a few additional files which need to be updated as well so that they work both as engines and in an app.
Wrap the blog engine initializer files so it does not interfere with the main rails app. So far it should just be.
As of right now the migration files also don't get copied over. So you may want to write some sort of task to handle that. Example below:
Next let's package it all together with a gemspec file to make it easy to load by other apps. Take a look at the sample.
You should now be able to build the gem. Now comes the task of actually adding it to another rails app. After creating a new rails app (see above), there are two standard ways you will probably want to include your engine. For development I would suggest using an absolute path. This will take advantage of rails request reloading and make it easy to make adjustments to code and files without repackaging and installing the whole engine after every change. It should also make it much easier to test within the context of the currently running application.
In staging and production however you will want to build your gem and eithier server it up via a gemserver or package it with the app. Use a simple command like
$ gem build blog_engine.gemspec
to build the gem. You can then eithier deploy to rubygems.org or if your running your own gemserver like stickler you should be able to easily push it their. Of course you also package it with your app depending on your needs.
If all went well all models, views, controllers and routes should be available in your app. We'll leave more advanced topics like name-spacing and dependencies for another article. In the meantime I highly suggest looking at this gist for more options and refactoring your current rails plugins into engines.
edit: Double loading caveat as of 2011-08-19
We recently ran into an issue with double loading of files in the apps / tasks / configs / etc directories when using the "gempsec" command in our Gemfile. This only manifests itself while running an engine as an app due to the fact that the app will load the gem first because it is required via bundler. This occurred because when both the application and the engine are loaded as of rails 3.0.9 the framework does not seem to check whether files have already been loaded. Whether or not this is by design is unclear but so far the easiest and cleanest way to work around this is that we've found so far is make the following changes:
1. Don't utilize the gemspec command in your Gemfile. Instead list out each dependencies. This is a bit of dependency duplication but nothing that major. The dependencies can always be added to another file to keep things dry.
2. Add any common logic between your engine and app into modules
3. Require your custom logic in both your application.rb and your engine.rb files.
If everything went well you will have retained all functionality without any duplication. This also keeps your code nicely modular and dry between your engine and app.