Note: This is an introduction explaining the concept of “acts_as”. This will not go into detail about creating the plugin.. only the basic description of the functionality of the plugin. Part 2 will go into detail about creating the structure outside of this and making it a real plugin.
You have seen them everywhere in the Rails world. Acts as this, Acts as that, Is able…. Most of these plugins have the same goal: Allow any ActiveRecord subclass to “have many” of another specific AR subclass. For example, acts_as_taggable_on allows a post, article, product, or any other AR model to have many tags. Hence the name “taggable”. These plugins take care of the associations for you, by adding only one extra database table to your schema. You don’t have to alter the “able” model’s table structure to make it work. “Able” plugins do this by using polymorphism. It is actually quite simple to create something that is “able” using Rails 3. In this tutorial, I will create the code directly in the Rails app, instead of using a gem (so as the introductory note states… this is not a real plugin yet). Check it out:
Create a plugin that allows any ActiveRecord model to have many reviews. Reviews will have a rating, a text review, pros, cons, and a user ID
1. Create the review Model
Easy enough to create using Rails generators:
rails g model review rating:integer review_text:text pros:string cons:string user_id:integer reviewable_type
2. Define the polymorphic associations in the model and migration
You will need to add this line to your migration in order to generate the 2 extra columns for polymorphism (reviewable_type,reviewable_id)
t.references :reviewable, :polymorphic => true
This will spit out our migration and create the Review model.
Next we will add the polymorphic association to the Review class that was just generated:
class Review < ActiveRecord::Base belongs_to :reviewable, :polymorphic=>true end
Basically, this tells Rails that you want to the Review to belong to “reviewable” and “reviewable” can be any other type of model. Behind the scenes, Rails just looks at the “reviews” table for “reviewable” using the “reviewable_type” and “reviewable_id”. This may be a little confusing if you are not familiar with polymorphic associations in Rails. Lets take a look at how the records actually show up in the table and it may make a little more sense:
id | reviewable_type | reviewable_id | review_text | 1 | "Product" | 99 | "my review of product #99" | 2 | "Article" | 25 | "my review of article #25" |
As you can see, each row represents a single review, but each review belongs to a different model instance. Review #1 belongs to Product #99, and review #2 belongs to Article #25. By creating the 2 “reviewable” columns, you are able to associate a review with more than one model class.
3. Run the migration
4. Create a module that ActiveRecord::Base will extend
This allows you to keep things DRY and simple in each model that is “reviewable” (has_many :reviews). You can drop this into your /app/models directory.
module Reviewable def is_reviewable has_many :reviews, :as=>:reviewable, :dependent=>:destroy include InstanceMethods end module InstanceMethods def reviewable? true end end end ActiveRecord::Base.extend Reviewable
First, lets look at the last line in the code above. When this Ruby file is evaluated it will make ActiveRecord::Base extend Reviewable. Basically when you extend a module, the class methods are added to the class that is extending the module. So ActiveRecord::Base now has a class method named “is_reviewable”. Since all your models extend ActiveRecord::Base, they will also respond to this class method.
Next take a look inside the class method. The “is_reviewable” method calls the “has_many” class method and also adds an instance method to the ActiveRecord::Base subclass instance. Lets take a look at a real example that would use the “is_reviewable” class method.
class Product < ActiveRecord::Base is_reviewable end
This used to look like some kind of magic to me when I was a Rails novice. Its just a nice DSL, but behind the scenes this line is calling a class method. Since this line is not in a “def”, the class method will be executed immediately when the class is loaded by Rails. in other words… self.is_reviewable. The class is loaded, the method is executed and has_many (also a class method) is executed and the methods defined in the InstanceMethods module are included in the Product class.
5. Try it out
For simplicity I will not include the tests here, but please note that it is important to create tests using Rspec (or whatever framework you prefer) for all of this code. These console lines could easily be translated into a test spec.
Lets see this in action in the Rails console:
#check to see if the class method is there for AR Base ActiveRecord::Base.methods.include?(:is_reviewable) => true #check to see if class method is there for Product (has to be b/c it subclasses AR Base) Product.methods.include?(:is_reviewable) => true #check to see if the instance method is there Product.instance_methods.include?(:reviewable?) => true product = Product.new => # product.reviewable? => true product.reviews =>  #nothing here yet.. we havent created any product.reviews.create(review_text: "test") => #this results in error.. you can create a review until the product has been saved product.save product.reviews.create(review_text: "test") => #<Review id: 1, reviewable_type: "Product", reviewable_id: 101 ....> product.reviews.size => 1
It looks a bit complicated and magical when you first encounter these plugins, but in reality its as simple as adding a class method to AR. Please read this blog by Yehuda Katz which shows how some people make the extends/includes thing alot more complicated than it should be. I was using the “overkill” way before I read his post. It totally makes sense and removes some of the confusion you may have when looking at the source of some of the “able” plugins.