Alternative to method_missing in Rails

Yes. it has been a long time since I’ve posted anything, but I’m sure I’ll want to refer to this in the future.

While developing some new functionality for TheBigFork, I found an opportunity to take advantage of the mysterious “method_missing” method in my ActiveRecord models. I have two models: Website and Setting. A website has many settings. Nothing earth-shattering there. To simplify the explanation, let’s just say I want to be able to load a value from the settings table with something like this:

website.site_name
=> "TheBigFork"

Where the “site_name” comes from the related settings. The longer, built-in way to do this is:

website.settings.where(:name => 'site_name').first.value
=> "TheBigFork"

Okay, so the “method_missing” approach would look something like this:

class Website < ActiveRecord::Base
  has_many :settings

  def method_missing(method)
    self.settings.where(:name => method).first.value
  end
end

(I really need to figure out a better way to post code in WordPress. Maybe it is time to move this blog to my own server where I can manage plugins. Anyway…)

That approach sort-of works. It seems to work better in some versions of rails than others. Some problems show up because “website.respond_to?” returns false for these pseudo methods. That could be fixed by adding this to the Website class:

def respond_to?(method)
  if self.method.to_s != 'settings' && self.settings.where(:name => method)
    true
  else
    super
  end
end

But that still gave me all sorts of “stack level too deep” errors. I stumbled upon Jay Field’s post, Replace method_missing with dynamic method definitions. Just what I needed. Except it wasn’t a Rails-specific solution. Just ruby in general. Apparently, classes that inherit from ActiveRecord::Base may or may not load with your own initialize method. So, I used the after_initialize hook like so:

class Website < ActiveRecord::Base
  has_many :settings
  after_initialize :dynamic_methods

  def dynamic_methods
    self.settings.each do |setting|
      (class << self; self; end).class_eval do
        define_method setting.name.to_sym do |*args|
          self.__send__("get_value", setting.name, *args)
        end
      end
    end
  end

  def get_value(name)
    self.settings.where(:name => name).first.value
  end

end

Works like a charm. It even answers correctly to the “respond_to?” for each setting.

Now, it is a bit expensive to dynamically create lots of methods every time you initialize a new instance, so I probably wouldn’t recommend this approach if you have tons of settings for each website. However, in my application, I actually have a third model involved (Brand) which shares settings among websites. The website actually caches the values, and this solution works nicely.