Posts Tagged plugin

Developing is_able or acts_as plugins for Rails 3 – Part 1

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.

Tags: , , , ,

Hooking in your Rails 3 Engine or Railtie Initializer in the right place

I am creating a Rails Engine and I have an initializer that needs to interact with the application routes.  I had no luck just adding the initializer like this:

require "my_engine"
require "rails"

module MyEngine
  class Engine < Rails::Engine
      initializer 'my_engine.interact_with_routes' do |app|
        # meat goes here
      end
  end
end

The initializer runs, but the problem is that the routes have not been initialized in the Application object and therefore my code does not have any routes to interact with. The rails core geniuses have included a feature that allows you to pick where your initilializer should load. You can add :after or :before params like so:

require "my_engine"
require "rails"

module MyEngine
  class Engine < Rails::Engine
      initializer 'my_engine.interact_with_routes', :after=>"some_other_initializer" do |app|
        # meat goes here
      end
  end
end

I didn’t really have a clue what initializers were running inside the rails core, so with some quick cowboy coding, I added some ugly print statements to “railties/lib/rails/application.rb” inside the initializers def, so I could see what the order of the initializers and where they were coming from:

    def initializers
      initializers = Bootstrap.initializers_for(self) #BOOTSTRAP
      print_initializers(initializers)
      railties.all { |r| initializers += r.initializers } #RAILTIES
      print_initializers(initializers)
      initializers += super #SUPER
      print_initializers(initializers)
      initializers += Finisher.initializers_for(self) #FINISHER
      print_initializers(initializers)
      initializers
    end

    def print_initializers(initializers)
      initializers.each do |i|
        p i.name
      end
      p "----------------------------"
    end

Here are the results (formatted with repeats removed):

BOOTSTRAP
:load_environment_config
:load_active_support
:preload_frameworks
:initialize_logger
:initialize_cache
:set_clear_dependencies_hook
:initialize_dependency_mechanism
:bootstrap_hook
RAILTIES
"i18n.callbacks"
"active_support.initialize_whiny_nils"
"active_support.deprecation_behavior"
"active_support.initialize_time_zone"
"action_dispatch.prepare_dispatcher"
"action_view.cache_asset_timestamps"
"action_view.javascript_expansions"
"action_view.set_configs"
"action_controller.logger"
"action_controller.initialize_framework_caches"
"action_controller.set_configs"
"action_controller.deprecated_routes"
"active_record.initialize_timezone"
"active_record.logger"
"active_record.set_configs"
"active_record.initialize_database"
"active_record.log_runtime"
"active_record.set_dispatch_hooks"
"active_record.add_concurrency_middleware"
"action_mailer.logger"
"action_mailer.set_configs"
"active_resource.set_configs"
:set_load_path
:set_autoload_paths
:add_routing_paths
:add_routing_namespaces
:add_locales
:add_view_paths
:load_config_initializers
:engines_blank_point
"my_engine.interact_with_routes"
SUPER
:set_load_path
:set_autoload_paths
:add_routing_paths
:add_routing_namespaces
:add_locales
:add_view_paths
:load_config_initializers
:engines_blank_point
FINISHER
:add_generator_templates
:ensure_autoload_once_paths_as_subset
:add_to_prepare_blocks
:add_builtin_route
:build_middleware_stack
:eager_load!
:finisher_hook
:disable_dependency_loading

Pretty cool stuff. You can see what order things initialize when a Rails server (or console or test) starts up.

I figured I would load my initializer last, to make sure that everything was available in the Application object, so here is my code:

require "my_engine"
require "rails"

module MyEngine
  class Engine < Rails::Engine
      initializer 'my_engine.interact_with_routes', :after=> :disable_dependency_loading do |app|
        # meat goes here
      end
  end
end

I ran a quick test and now my code can see all the routes in my plugin and my rails app. Hopefully this list will be useful when trying to figure out where to place your initializer. Holla!

UPDATE: upon closer inspection it appears that the initializer that loads the routes is :build_middleware_stack inside the finisher.. so putting your route dependant initializer after that initializer should suffice.

Tags: , , ,