Last updated

How to write a Rails Plugin (for controllers)

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:

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

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:

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

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

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

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:

1./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.

1acts_as_exportable
2acts_as_exportable :book

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

1module ActsAsExportable
2end

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.

 1module ActsAsExportable
 2  def self.included(base)
 3    base.extend(ClassMethods)
 4  end
 5
 6  class Config
 7    attr_reader :model
 8    attr_reader :model_id
 9
10    def initialize(model_id)
11      @model_id = model_id
12      @model = model_id.to_s.camelize.constantize
13    end
14
15    def model_name
16      @model_id.to_s
17    end
18  end
19
20  module ClassMethods
21	  def acts_as_exportable(model_id = nil)
22      # converts Foo::BarController to 'bar' and FooBarsController
23      # to 'foo_bar' and AddressController to 'address'
24      model_id = self.to_s.split('::').last.sub(/Controller$/, '').\
25 pluralize.singularize.underscore unless model_id
26
27      @acts_as_exportable_config = ActsAsExportable::Config.\
28 new(model_id)
29      include ActsAsExportable::InstanceMethods
30    end
31
32    # Make the @acts_as_exportable_config class variable easily
33    # accessable from the instance methods.
34    def acts_as_exportable_config
35      @acts_as_exportable_config || self.superclass.\
36 instance_variable_get('@acts_as_exportable_config')
37    end
38  end
39end

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.

 1module InstanceMethods
 2  def export_to_xml
 3    data = Book.find(:all, :order => 'title', :conditions => conditions_for_collection)
 4    send_data data.to_xml,
 5      :type => 'text/xml; charset=UTF-8;',
 6      :disposition => "attachment; filename=books.xml"
 7  end
 8
 9  # Empty conditions. You can override this in your controller
10  def	conditions_for_collection
11  end
12end

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:

1self.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:

 1module ActsAsExportable
 2  def self.included(base)
 3    base.extend(ClassMethods)
 4  end
 5
 6  class Config
 7    attr_reader :model
 8    attr_reader :model_id
 9
10    def initialize(model_id)
11      @model_id = model_id
12      @model = model_id.to_s.camelize.constantize
13    end
14
15    def model_name
16      @model_id.to_s
17    end
18  end
19
20  module ClassMethods
21	  def acts_as_exportable(model_id = nil)
22      # converts Foo::BarController to 'bar' and FooBarsController to 'foo_bar'
23      # and AddressController to 'address'
24      model_id = self.to_s.split('::').last.sub(/Controller$/, '').\
25 pluralize.singularize.underscore unless model_id
26
27      @acts_as_exportable_config = ActsAsExportable::Config.new(model_id)
28      include ActsAsExportable::InstanceMethods
29    end
30
31    # Make the @acts_as_exportable_config class variable easily
32    # accessable from the instance methods.
33    def acts_as_exportable_config
34      @acts_as_exportable_config || self.superclass.\
35 instance_variable_get('@acts_as_exportable_config')
36    end
37  end
38
39	module InstanceMethods
40	  def export_to_xml
41	    data = self.class.acts_as_exportable_config.model.find(:all,
42              :order => 'title',
43              :conditions => conditions_for_collection)
44	    send_data data.to_xml,
45	      :type => 'text/xml; charset=UTF-8;',
46	      :disposition => "attachment; filename=\
47 #{self.class.acts_as_exportable_config.model_name.pluralize}.xml"
48	  end
49
50	  # Empty conditions. You can override this in your controller
51    def	conditions_for_collection
52    end
53  end
54end

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)

1acts_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’:

1ActionController::Base.class_eval do
2  include ActsAsExportable
3end

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.