Building a plugin (form_for and overriding methods continued)

In my last post, we looked at why Rails doesn’t use getter methods in form_for. We also came up with a relatively ugly fix. In this post, we’ll clean that code up and turn it into a plugin.

Previously, we figured out that we could override respond_to? to make form_for use getter methods on our Rails models. We ended up with the following code to do this for our pending_email method.

def respond_to?(*args)
  if args.first.to_s == "pending_email_before_type_cast"
    false
  else
    super
  end
end

We will make our code more modular in a couple of steps. First, let’s change our respond_to? method to deal with an array of column names instead of a hard coded value. We could easily do something like:

def respond_to?(*args)
  if @use_getters.include?(args.first.to_s)
    false
  else
    super
  end
end

That code would work, but there is a problem. Notice that @use_getters is an instance variable. We probably don’t want to create a variable to hold the list of columns to use getters for on every model object. Instead, we could store that variable in a class variable and create accessors for it. We probably won’t ever remove an item from this list of attributes, let’s create two methods add_method_to_use_getter and methods_to_use_getters. There’s nothing tricky in these methods as you can see:

 
  def self.add_method_to_use_getter(method)
    @methods_to_use_getters ||= []
    @methods_to_use_getters << method.to_s+"_before_type_cast"
  end

  def self.methods_to_use_getters
    @methods_to_use_getters || []
  end

In our code, we first make sure that our array exists. If it does, we add the name of the method to hide to our array. In the next method, we return either the array of method names or an empty array. Next, we can modify our respond_to? method.

def respond_to?(*args)
  if self.class.methods_to_use_getters.include?(args.first.to_s)
    false
  else
    super
  end
end

With that done, we have just a couple of remaining steps to make this look nice. First, let’s create make it so that the following code will work:

class User < ActiveRecord::Base
  use_getters_for :pending_email, :other_attribute
end

Doing that is easier than you might think. All we need is a use_getters_for method that takes a variable number of arguments. That is simply:

  def self.use_getters_for(*args)
    args.each {|a| add_method_to_use_getter a}
  end

Next, let’s turn this into a plugin so that we don’t have to constantly define all of those methods on our classes. To create a plugin, we’ll start by running script/generate plugin uses_getters_for_forms to create a skeleton plugin. Next, we’ll create a couple of modules inside the generated lib/uses_getters_for_forms.rb. We will need to create two modules. One for instance methods and one for class methods. (Including a module only adds instance methods. This is explained brilliantly in Patrick Farley’s Mountain West Ruby Conf talk.) To avoid naming conflicts, we will put both of these modules into the UsesGettersForForms module.

# UsesGettersForForms
module UsesGettersForForms
  module ClassMethods
    def use_getters_for(*args)
      args.each {|a| add_method_to_use_getter a}
    end
    def add_method_to_use_getter(method)
      @methods_to_use_getters ||= []
      @methods_to_use_getters << method.to_s+"_before_type_cast"
    end

    def methods_to_use_getters
      @methods_to_use_getters || []
    end
  end
  module InstanceMethods
    def respond_to?(*args)
      if self.class.methods_to_use_getters.include?(args.first.to_s)
        false
      else
        super
      end
    end
    
  end
end

With that done, we just need to make that method available to all of our models. We customarily do that by including it in ActiveRecord::Base. We can do that for our class methods, but not for our instance methods. As Patrick Farley explains, Our included respond_to? method won’t be called until after the Rails method is called. Tow work around this, we’ll dynamicly include our respond_to? method only when we need to. Here is the code to put into init.rb to do this.

# Include hook code here
require 'uses_getters_for_forms'
ActiveRecord::Base.send(:extend,UsesGettersForForms::ClassMethods)

Finally, we need to modify our use_getters_for method to add the respond_to? method the first time this is called.

# UsesGettersForForms
module UsesGettersForForms
  module ClassMethods
    def use_getters_for(*args)
      unless @ugff_loaded
        include ::UsesGettersForForms::InstanceMethods
        @ugff_loaded=true
      end
      args.each {|a| add_method_to_use_getter a}
    end
    def add_method_to_use_getter(method)
      @methods_to_use_getters ||= []
      @methods_to_use_getters << method.to_s+"_before_type_cast"
    end

    def methods_to_use_getters
      @methods_to_use_getters || []
    end
  end
  module InstanceMethods
    def respond_to?(*args)
      if self.class.methods_to_use_getters.include?(args.first.to_s)
        false
      else
        super
      end
    end
    
  end
end

There’s a lot of code there, and a lot of it is somewhat tricky. Don’t just copy and paste this code. Understand why it works. Watch the video by Patrick Farley and play with the code. Next, we’ll look at why this breaks for STI models and how we can fix it.