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:
Requirement:
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
Implementation:
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
rake db:migrate
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
Conclusion
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.




#1 by Joshua Pinter on February 3, 2011 - 12:05 pm
Quote
Hey Cowboy.
Thanks for the super clear and concise explanation of creating a simple plugin. Took me a while to find this.
You might also want to add a line about where to put the file created in step 4. I tried adding it into the main lib directory with no success. So I ended up adding it to the initializers directory.
Is that the best spot do you think?
Cheers,
JP
#2 by cowboycoded on February 3, 2011 - 12:44 pm
Quote
Joshua, it really depends on what you are using. Rails 3 is not going to autoload the classes in your “lib” dir. If you are on Rails 2, then I think that class in /lib should work. Also, you could probably just put it into the /app/models directory since these files are autoloaded. I think the cleanest method is to put the module in /app/models/ and the remove the last line and put it in an initializer.
Ideally, you don’t want to put this directly in your app… it should be a gem. If you are on Rails 3, then you should make it a Railtie or an Engine plugin. I am working on a followup post that I should have up by Monday, which will show you how to create a Rails 3 Engine with this code. The modest rubyist has a very good tutorial on creating engines, if you don’t want to wait. It was written while Rails 3 was in beta, but I think it is still relevant. There are 3 parts to this tutorial:
http://www.themodestrubyist.com/2010/03/01/rails-3-plugins—part-1—the-big-picture/
#3 by Joshua Pinter on February 3, 2011 - 5:35 pm
Quote
Many thanks. I’ll check out the tutorial and give yours a read as well.
I’m on Rails 2 and putting it in the lib directory didn’t work. I think you’re right, putting it in the model directory makes the most sense. I’ll give that a go for now and see if I can’t make it a gem later on.
Thanks again.
JP
#4 by JohnK on February 7, 2011 - 11:40 am
Quote
Very helpful.. — Love the polymorphic magic. Thanks!
#5 by Marko on January 6, 2012 - 7:35 am
Quote
Hi cowboy! This is best article I googled so far.. Can you provide us link to part 2 if you made it? I’m new to plugins world, and I’m kinda sad it’s so difficult to understand
#6 by cowboycoded on January 6, 2012 - 12:09 pm
Quote
I never did get around to writing part 2 on my blog, but I did a gem workshop for our local Ruby Meetup group. Here is the tutorial I wrote for that:
http://charlotte-ruby.github.com/gem_workshop_tutorial/
Hope that helps.. if not, you can comment on this post with any questions you might have and I might be able to point you in the right direction
#7 by Marko on January 13, 2012 - 11:43 am
Quote
Thanks! Well, my main question is how to pack it as a plugin? I guess step 4 would be modified a little. So, everything you wrote, but made as plugin and not as part of main app.
Is polymorphic association required for act_as/is_able plugins?
#8 by Alban on April 25, 2012 - 4:26 pm
Quote
Thanks for this article. I’ll probably dive in the source to find out how the DSL for inclusion works. Note that the ActiveSupport::Concern make the module more readable.
#9 by Anand on April 27, 2012 - 7:03 am
Quote
Awesome post, thanks!
#10 by Simon Bettison on October 5, 2012 - 3:12 am
Quote
link in #2 has moved to here:
http://blog.loopedstrange.com/modest-rubyist-archive/rails-3-plugins-part1-the-big-picture
#11 by Mark on January 20, 2013 - 6:58 am
Quote
I don’t understand the purpose of the reviewable? instance method (unless it’s just for testing purposes). You’re going to just get a NoMethodError if you call it on a model on which is_reviewable hasn’t been called.
#12 by Anthony on March 26, 2013 - 7:38 am
Quote
This post was useful. I converted an old Rails plugin to a Rails 3.1+ gem. If anyone wants to see the example, it is here: https://github.com/toptierlabs/acts_as_fulltextable