Tuesday, May 27, 2008

depends_on

For one of my projects I had the need for a polymorphic association in which one object required the existence of the other. I had written a specific version of this, but decided it could easily be abstracted. I didn't go with the built in ActiveRecord polymorphic features as I wanted to strip it down to the basics to see if it would work. Why? I would love to use this in DataMapper at some point.

module Stonean
module DependsOn
def self.included(base)
base.extend Stonean::DependsOn::ClassMethods
end

module ClassMethods
def depends_on(model_sym, options = {})
define_relationship(model_sym,options)

validates_presence_of model_sym
validates_associated model_sym

# Before save functionality to create/update the requisite object
define_save_method(model_sym, options[:as])

define_find_method(model_sym)


options[:attrs].each{|attr| define_accessors(model_sym, attr)}
end

#model_instance = instance_variable_get("@#{model_sym}")
private
def define_relationship(model_sym, options)
if options[:as]
has_one model_sym, polymorphic_constraints(options[:as])
else
belongs_to model_sym
end
end

def define_save_method(model_sym, polymorphic_name = nil)
define_method "save_requisite_#{model_sym}" do
if polymorphic_name
eval("self.#{model_sym}.#{polymorphic_name}_type = self.class.name")
eval("self.#{model_sym}.#{polymorphic_name}_id = self.id")
end

eval("self.#{model_sym}.save")
end

before_save "save_requisite_#{model_sym}".to_sym
end

def define_find_method(model_sym)
self.class.send :define_method, "find_with_#{model_sym}" do |*args|
eval <<-CODE
if args[1] && args[1].is_a?(Hash)
if args[1].has_key?(:include)
inc_val = args[1][:include]
new_val = inc_val.is_a?(Array) ? inc_val.push(:#{:model_sym}) : [inc_val, :#{model_sym}]
args[1][:include] = new_val
else
args[1].merge({:include => :#{model_sym}})
end
else
args << {:include => :#{model_sym}}
end
find(*args)
CODE
end
end

def define_accessors(model_sym, attr)
define_method attr do
eval("self.#{model_sym} ? self.#{model_sym}.#{attr} : nil")
end

define_method "#{attr}=" do |val|
model_defined = eval("self.#{model_sym}")

unless model_defined
klass = model_sym.to_s.classify
eval("self.#{model_sym} = #{klass}.new")
end

eval("self.#{model_sym}.#{attr}= val")
end
end

def polymorphic_constraints(polymorphic_name)
{ :foreign_key => "#{polymorphic_name}_id",
:conditions => "#{polymorphic_name}_type = '#{self.name}'"}
end

end # ClassMethods
end # DependsOn module
end # Stonean module
ActiveRecord::Base.send :include, Stonean::DependsOn


Here's an example, but first a little information. Content is a model that all "presentable" objects in my cms depend on. It holds the name and url attributes so when you are calling message.name, you are really calling message.content.name.
class Message < ActiveRecord::Base
depends_on :content, :attrs => [:name, :url], :as => :presentable
end

This means your form and views can use these methods and not worry about the underlying association. for example:
# This form tag was used for descriptive purposes.
text_field_tag "message[name]", message.name

Now you don't have to do anything special in your views or controller to build and save the depends_on object.

You also get a find_with_<dependent_model> class method. This means if you have a Picture model that depends_on Image, you would get a Picture.find_with_image method.

This example is very specific, but with a little modification, can be used to implement a class table inheritance architecture. I definitely plan on doing this soon.

I will be releasing this as a gem in the near future, but for now you can just copy the code above and add it into your config/initializers directory.

If you have any ideas or suggestions, I would love to hear them.

thanks,
andy

0 comments: