Saturday, January 5, 2008

Developing plugin with rails-engine 1.2 and Rails 1.2.x

After a much awaited opportunity to write a self contained plugin for rails application I ultimately got this chance with my new rails project where a new subsystem had to build for an already existing intranet application. The first thing I tried was to google around for some introductory articles or blog-posts about writing plugins using rails-engines. The ones I found were either outdated, or slightly confusing with vague description. So, I took this as a golden opportunity and decided to go ahead with this blog-post. At the time of writing of this blog v2.0 of engines plugin is available, revamped for using with Rails-2.0. I can't use it simply because the Rails application with which my plugin would ultimately integrate is live and running on v1.2.3.

Assuming that the required rails (1.2.x) application is there, we begin with the installation of engines plugin first.

1. Install engines plugin

railsroot$ ruby script/plugin install http://svn.rails-engines.org/plugins/engines
That's all with the installation part. With engines 1.2 no extra configuration is required in config/environment.rb. The only thing which is recommended but not required is to keep the loading order of engines plugin first before all the other plugins. This is possible starting with rails 1.2 using
config.plugins = ['engines', '*']
option in either config/environment.rb, or config/environments/*.

2. Create new plugin

railsroot$ ruby script/plugin generate myplugin
This will generate the following folder structure within root of rails application.
vendor/
  |
  plugins/
    |
    myplugin/
      |- lib/
      |    |- myplugin.rb
      |
      |- tasks/
      |    |- myplugin_tasks.rake
      |
      |- test/
      |    |- myplugin_test.rb
      |
      |- README
      |- Rakefile
      |- init.rb
      |- install.rb
      |- uninstall.rb

3. Create app, app/controllers, app/models, app/helpers, app/views, db, db/migrate, and assets folders within the root of newly created plugin.

railsroot$ mkdir vendor/plugins/myplugin/app
railsroot$ mkdir vendor/plugins/myplugin/app/controllers
railsroot$ mkdir vendor/plugins/myplugin/app/models
railsroot$ mkdir vendor/plugins/myplugin/app/helpers
railsroot$ mkdir vendor/plugins/myplugin/app/views
railsroot$ mkdir vendor/plugins/myplugin/db
railsroot$ mkdir vendor/plugins/myplugin/db/migrate
railsroot$ mkdir vendor/plugins/myplugin/assets
All the above created folders follow the folder structure of rails. `assets` folder corresponds to the pubic folder of the rails. It acts as a repository for all the plugin resources like images, stylesheets, javascripts etc. When rails starts then engines plugin replicates the content of this folder into public/plugin_assets/myplugin folder. All the resources in plugin's assets folder can be referred anywhere (generally views) using the extended Rails' helpers enhanced by the engines plugin to accept a :plugin option, indicating the plugin containing the desired resource.
# Include stylesheet from plugin
<%= stylesheet_link_tag 'mystyles', :plugin => 'myplugin', :media => 'screen' %>

# Include plugin javascript
<%= javascript_include_tag 'myfuncs', :plugin => 'myplugin' %>

# Incorporate plugin images
<%= image_tag 'logo.jpg', :plugin => 'myplugin' %>
<%= image_path 'header.jpg', :plugin => 'myplugin' %>

4. Migrations
Plugin migrations are required to generate tables for plugin models that can be shared with rest of the rails application. The creation of plugin migrations is similar to that of with rails. The only thing which needs to know is how to execute them. Unlike normal rails migrations, plugin migrations are not meant to be executed directly from within plugin. They are to be first migrated into the main migration stream using

railsroot$ ruby script/generate plugin_migration
in order to accurately reflect the state of appplication's database. Suppose according to the schema_info table in database the rails application is at version 35 and myplugin plugin contains a single migration 001_create_testplugin_table.rb in vendor/plugins/myplugin/db/migrate folder. Executing plugin_migration script
railsroot$ ruby script/generate plugin_migration
     exists  db/migrate
     create  db/migrate/036_create_testplugin_table_to_version_1.rb
will take rails applications to version 36 by adding a new migration 036_create_testplugin_table_to_version_1.rb to main migration stream i.e. migration sequence in railsroot/db/migrate folder. The content of this new migration would be something like this:
class CreateTestpluginTableToVersion1 < ActiveRecord::Migration
  def self.up
    Engines.plugins[:myplugin].migrate(1)
  end

  def self.down
    Engines.plugins[:myplugin].migrate(0)
   end
end
When the application is migrated up using rake db:migrate, then plugin will be migrated to its latest version (1 here). This can be verified by the new plugin tables in database. If at any time later it is required to rollback the application back to version 35 then it can be simple done by using
rake db:migrate VERSION=35
which will drop all the tables from myplugin migration (if specified in self.down method). 5. Generate all the controllers, models, views, and helpers for the plugin, exactly as we do in Rails, but this time only in context of the plugin.
vendors/
  |
  plugins/
    |
    myplugin/
      |
      app/
        |- controllers/ # Put all your plugin controllers here
        |- models/      # plugin models
        |- helpers/     # plugin helpers
        |- views/       # plugin views

6. Add routes for plugin

railsroot$ touch vendor/plugins/myplugin/routes.rb
Add all the required routes for the plugin.
connect '/login', :controller => 'myplugin/account', :action => 'login'

# add a named route
logout '/logout', :controller => 'myplugin/account', :action => 'logout'

# some restful stuff
resources :things do |t|
  t.resources :other_things
end
Don't prepend map (map.resource, or map.connect) with any of the routes. Also don't copy paste the application's routes.rb for plugin as the invocation of draw() method in it erases all the previous routes before adding the new ones. I had to struggle really hard to understand this minor stuff in lack of proper documentation. Add plugins' routes within routes of main application.
ApplicationController::Routing::Routes.draw do |map|

  map.connect '/app_stuff', :controller => 'application_thing' # etc...

  # This line includes the routes from the given plugin at this point, giving you
  # control over the priority of your application routes
  map.from_plugin :myplugin

  map.connect ':controller/:action/:id'
end
Be careful while doing this as the placement of this statement will directly affect the priority (Rails interpretation) of routes in the application.

No comments: