Ariejan.net has moved and is now available at https://www.devroom.io

How to write a Rails Plugin (for controllers)

6 July 2007

A few days back I posted my very first Rails plugin, Acts As Exportable. Although writing a plugin is rather easy, you must know a few tricks on how to get things going.

This article will show you how to develop a plugin that adds functionality to a controller (other plugins, e.g. for models) will follow later. In fact, I’ll explain to you how I developed my Acts As Exportable plugin.

Let’s take a basic Rails application for starters. You have setup a model with some attributes and a scaffolded controller that allows you to CRUD your items. In this tutorial I’ll be working with books. The model is named ‘Book’ and the controller ‘BooksController’. Start your web server now and add some random data to play with.

Before you dive into writing a plugin for the controller to export data to XML you should have some basic functionality in your controller first. I’ve found it easier to develop my code in the controller first, and then port it to a plugin.

So, add a new method to your BooksController that’ll export books to XML. This looks quite easy:

def export_to_xml
  books = Book.find(:all, :order => 'title')
  send_data books.to_xml,
    :type => 'text/xml; charset=UTF-8;',
    :disposition => "attachment; filename=books.xml"
end

Now, call /books/export_to_xml and you download a real XML file containing all your books! To make things a bit more complicated, we want to be able to feed this method some conditions to select books. A nice solution is to add a special method for this that defines these conditions. (You could also use them in listing books, for example.) I add a new method to the BooksController:

def conditions_for_collection
  ['title = ?', 'some title!']
end

The condition is of the same format you can feed to find. Here you could, for example, select only the books belonging to the currently logged in user.

Next, update the export_to_xml method to use these conditions

def export_to_xml
  books = Book.find(:all, :order => 'title', :conditions => conditions_for_collection)
  send_data books.to_xml,
    :type => 'text/xml; charset=UTF-8;',
    :disposition => "attachment; filename=books.xml"
end

Nice that’s it. Now, you like what you’ve made so far, and want to stuff it into a plugin and put it on your weblog. Here’s how to go about that.

Creating the plugin

First, generate the basic code for a plugin:

./script/generate plugin acts_as_exportable

This will create a new directory in vendor/plugins containing all the basic files you need. First, we’ll take a look at vendor/plugins/acts_as_exportable/lib/acts_as_exportable.rb. This is where all the magic happens.

What we want is to is add a method to ActionControllerBase that allows you to easily enable the plugin in a certain controller. So, how do you want to activate the plugin? Right, you just call ‘acts_as_exportable’ from the controller, or optionally, you add the name of the model you want to use.

acts_as_exportable
acts_as_exportable :book

The vendor/plugins/acts_as_exportable/lib/acts_as_exportable.rb contains a module that’s named after our plugin:

module ActsAsExportable
end

Next, we add a module named ‘ClassMethods’. These class methods will be added to ActionController::Base when the plugin is loaded (we’ll take care of that in a moment), and enable the functionality described above.

module ActsAsExportable
  def self.included(base)
    base.extend(ClassMethods)
  end

  class Config
    attr_reader :model
    attr_reader :model_id

    def initialize(model_id)
      @model_id = model_id
      @model = model_id.to_s.camelize.constantize
    end

    def model_name
      @model_id.to_s
    end
  end

  module ClassMethods
	  def acts_as_exportable(model_id = nil)
      # converts Foo::BarController to 'bar' and FooBarsController
      # to 'foo_bar' and AddressController to 'address'
      model_id = self.to_s.split('::').last.sub(/Controller$/, '').\
 pluralize.singularize.underscore unless model_id

      @acts_as_exportable_config = ActsAsExportable::Config.\
 new(model_id)
      include ActsAsExportable::InstanceMethods
    end

    # Make the @acts_as_exportable_config class variable easily
    # accessable from the instance methods.
    def acts_as_exportable_config
      @acts_as_exportable_config || self.superclass.\
 instance_variable_get('@acts_as_exportable_config')
    end
  end
end

So, what happened? The first method you see extends the current class (that’s every one of your controllers with the methods from the ClassMethods module).

Every class now has the ‘acts_as_exportable’ method available. What does it do? The plugin automatically grabs the name of the model associated (by convention) with the controller you use, unless you specify something else.

Next, we create a new configuration object that contains information about the model we’re working with. Later on this can contain more detailed information like what attributes to include or exclude from the export.

Finally we include the module InstanceMethods, which we still have to define. The instance methods are only included when we enable the plugin. In our case, the instance methods include the ‘export_to_xml’ and ‘conditions_for_collection’ methods. We can simply copy/paste them into your plugin.

module InstanceMethods
  def export_to_xml
    data = Book.find(:all, :order => 'title', :conditions => conditions_for_collection)
    send_data data.to_xml,
      :type => 'text/xml; charset=UTF-8;',
      :disposition => "attachment; filename=books.xml"
  end

  # Empty conditions. You can override this in your controller
  def	conditions_for_collection
  end
end

Take note that we don’t want to define any default conditions, because we don’t know what model we’re using here. By adding an empty method, the method is available and no conditions are used. Another developer can define ‘conditions_for_collection’ in his controller to override the one we write here.

In the ‘export_to_xml’ there are a few changes as well. First of all, I generalized ‘books’ to ‘data’.

The most important step is yet to come. We have still application specific code in your plugin, namely the Book model. This is where the Config class and @acts_as_exportable_config come in.

We have added a class variable to the controller named @acts_as_exportable_config. By default, this variable is not accessable by instance methods, so we need a little work around:

self.class.acts_as_exportable_config

This will call the class method ‘acts_as_exportable_config’ we defined in ClassMethods and return the value of @acts_as_exportable_config.

Note that we store the configuration in each seperate controller. This allows acts_as_exportable to be used with more than one controller at the same time.

With the model name made application independent, the whole plugin code looks like:

module ActsAsExportable
  def self.included(base)
    base.extend(ClassMethods)
  end

  class Config
    attr_reader :model
    attr_reader :model_id

    def initialize(model_id)
      @model_id = model_id
      @model = model_id.to_s.camelize.constantize
    end

    def model_name
      @model_id.to_s
    end
  end

  module ClassMethods
	  def acts_as_exportable(model_id = nil)
      # converts Foo::BarController to 'bar' and FooBarsController to 'foo_bar'
      # and AddressController to 'address'
      model_id = self.to_s.split('::').last.sub(/Controller$/, '').\
 pluralize.singularize.underscore unless model_id

      @acts_as_exportable_config = ActsAsExportable::Config.new(model_id)
      include ActsAsExportable::InstanceMethods
    end

    # Make the @acts_as_exportable_config class variable easily
    # accessable from the instance methods.
    def acts_as_exportable_config
      @acts_as_exportable_config || self.superclass.\
 instance_variable_get('@acts_as_exportable_config')
    end
  end

	module InstanceMethods
	  def export_to_xml
	    data = self.class.acts_as_exportable_config.model.find(:all,
              :order => 'title',
              :conditions => conditions_for_collection)
	    send_data data.to_xml,
	      :type => 'text/xml; charset=UTF-8;',
	      :disposition => "attachment; filename=\
 #{self.class.acts_as_exportable_config.model_name.pluralize}.xml"
	  end

	  # Empty conditions. You can override this in your controller
    def	conditions_for_collection
    end
  end
end

Add the following line to your BooksController and restart your web server. (Oh, and make sure to remove the export_to_xml method from the controller as well)

acts_as_exportable

Done! – Or not?

Enabling the plugin by default

We have a very nice plugin now, but it is not loaded by default! If you take a look at your plugin directory, you’ll find a file named ‘init.rb’. This file is executed when you (re)start your web server. This is the perfect place to add our class methods to the ActionController::Base. Just add the following three lines of code to ‘init.rb’:

ActionController::Base.class_eval do
  include ActsAsExportable
end

When we include our module, the ‘self.included’ method is called, and the ClassMethods module is added, thus enabling the acts_as_exportable method.

That’s all! Happy plugin writing!

Feel free to comment on this post and write about any of your own plugin (for controllers) experiences.