Every once in a while I find myself in the situation where I need to override a model or create an inheritance without using single table inheritance (STI).
The method_missing call can help us to create an intelligent router that will route calls to the parent class or the child while maintaining an extremely small code footprint.
Here’s an example of what I’m talking about. We needed a landing page that would be dynamic for each user that wanted one. But we needed a template landing page which just needed the values substituted in at the right spot. Here’s a quick Yaml of what the table might look like.
# Example Page landing_page_template: title: Welcome to [COMPANY-NAME] meta_keywords: [COMPANY-NAME], [COMPANY-META-KEYWORDS] ... # Example Landing Page landing_page: company_name: CV, Inc. company_logo: www.chuckvose.com/fake_logo.gif company_meta_keywords: happy, rails, method_missing, inheritance ...
So here’s the hope, when we load up the LandingPage object and call LandingPage#title it should display “Welcome to CV, Inc.”
Pseudo-code and implementation
def method_missing(method_name, args*) if method_name is in the extended database call method_name if method_name isn't in the extended database try calling parent.method_name return results directly or mangle them somehow
We chose to mangle the results but if you just wanted to add a couple columns you could. You could run into performance issues doing this instead of STI. However this is an absolutely ideal place to mangle the code too if you want. Probably as good as a controller after_filter.
Two steps needed to happen for this, we need to load up the template on initialization, then use method_missing to switch between calls to the LandingPage class (such as :company_name) and the Page class (such as title).
Step 1: Load the template
The first part is a little weird because of rails performance. If you haven’t used after_find/after_initialize rails-core had to put in a little kludge for performance reasons. The solution is just to define a blank function named after_find or after_initialize.
class LandingPage < ActiveRecord::Base after_initialize :find_template # Required to activate the after_initialize filter def after_initialize; end private def find_template @template = Page.find_by_url("landing_page_template") end end
Step 2: Create the method_missing switcher
The method_missing switcher had to incorporate two things and one trick. First, it needed to look in the LandingPage table for its own attributes. Then it needed to look in the Page table for anything else before passing it through a regex engine and returning. The last little trick was that the regex engine needed to only be run on Strings, trying to regex a boolean doesn’t work very well. This allows the plethora of query functions such as Page.valid?.
class LandingPage < ActiveRecord::Base private def method_missing(meth, *args) # See if this attribute is in this object already. # If so just call out to super and let # ActiveRecord::Base deal with trying to find the # attribute in the database. if @attributes.include?(meth.to_s) || @attributes.include?(meth.to_s.gsub(/(=|_before_type_cast)/, "")) super else # Call Page#meth and store the response. Usually something like @page.body or @page.title resp = @template.send(meth) # If the attribute returned from above is a String, toss it into the regex grinder # and return the results. if resp.is_a?(String) return resp.html_sub(self) # If the response is a boolean, symbol, or something else, just return it since we # can't (and probably don't need to) regex it. else return resp end end end end
Step 3: The regex grinder
Security Warning: Before I begin this I want people to know that it should never be used by people outside your direct trust. It calls ruby code directly off of text found in the database. It can be rewritten to be more secure but this wasn’t necessary for us. If you want slightly more security drop some constraints on the LandingPage.send() method. For even more security do one regex per tag to replace (such as html = html.gsub(/\[MERCHANT-NAME\]/, template.merchant_name || ""). Security Warning
The last step is totally up to you. We needed it to find things like [MERCHANT-NAME] and replace them with LandingPage#merchant_name. This can be done fairly simply monkey-patching String with a blocked gsub.
class String # Look for snippets like [MERCHANT-NAME] and replace # them with LandingPage#merchant_name def html_sub(landing_page) html = self # self cannot and should not be modified directly. # Look for [WHATEVER] tags html = html.gsub(/\[[^\]]+?\]/) do |match| # For each match, call the LandingPage instance method # with the same name and return those results or the empty # string. landing_page.send(match.gsub(/[\[\]]/, "").gsub(/-/, "_").downcase) || "" end end end
The method_missing call seems to be a rough spot for a lot of discussions. Why_ wrote an article (The Best of Method Missing) that shows this pretty well: he starts by explaining how he has hated every piece of code he’s written with it then proceeds to point out some method_missing code that he loves. Lots of people feel this way about it and about many aspects of Rails in general.
But sometimes it’s a wonderful thing. This example should have taken several hundred lines of code and lots of kludges and bugs, but method missing trimmed it to ~20 LOC and we haven’t been able to find a bug yet.
Hope this helps you with some project, I’ve enjoyed playing with it and writing it up.