Polymorphism and belongs_to: trying to override belongs_to


#1

Here is my story:

I have two models that represent people or groups that can “own”
things. One is Person, the other is Group. Then, I have stuff that can
be owned by either a Person or a Group, like a Post for instance:

class Post
belongs_to :owner, :polymorphic => true
end

class Person
has_many :posts, :as => :owner
end

class Group
has_many :posts, :as => :owner
end

For my own reasons, I have a form where I create/edit a Post,
including selecting the owner (yes, it may seem odd to have to
“select” an owner, but that is the way it is). I first tried using the
‘select’ form helper, attaching it to the owner attribute:

…<%= f.select :owner, @owners.map { |o| [o.displayname, o.id] }
%>…

I soon realized my obvious mistake, I need both the id AND type for
the owner. So, I created a read/write custom attribute for Post:

def owner_string
“#{owner.id}:#{owner.class.base_class}” # base_class just in case
owner using STI
end
def owner_string=(ref)
i, t = ref.split(’:’)
owner = eval(t).find(i.to_i)
end

Then I updated my view code to use my pseudo-attribute: owner_string
instead…

…<%= f.select :owner_string, @owners.map { |o| [o.displayname, “#
{o.id}:#{o.class.base_class}”] } %>…

This seems to work all right. However, I’ve got a lot of models that
can be “owned” just like the Post class. I’ve also got polymorphism
going on in a few other places.

So, to avoid repetition, my next step was to write some code, throw it
in config/initializers, and use it to extend ActiveRecord::Base. I’ll
include the code at the bottom and explain the problem here. My code
adds an instance method to AR::Base called “polymorphic” that returns,
for that instance a string holding the instances id and type (the base
AR type). Also, there is a class AR::Base method also named
“polymorphic” that takes a string containing an id/type as previously
shown above (“id:type”) and loads the instance if it exists.

Finally I tried to “override” (not sure if this is the correct ruby
term for this) the belongs_to macro method so that, when called (in my
model definitions) it will first perform the default behavior, then it
will detect if the belongs_to call is for a polymorphic association,
and if so, also create pseudo attribute methods such as “owner_string”
and “owner_string=(val)” (replacing “owner” w/the association name, of
course) so that I can immediately have these “attributes” as I
demonstrated above.

That last part, “overriding” belongs_to is what I can’t make work. The
following code works with the exception of the alias_method_chain
call. There doesn’t appear to be a method named belongs_to in
AR::Base.

So, anyone know how things really work here? How, perhaps, I can get
this to work? I also welcome general commentary/insights as to how I
can better deal with any of the above issues.

I’m very grateful, in advance, to anyone who actually reads this whole
thing and responds.

WARNING: Long code listing (excuse the verbose param-checking used for
debugging)
---------- file: config/initializers/polymorphic_references.rb

module PolymorphicReferences
module ActiveRecord
def self.included(base)
base.send :extend, ClassMethods
base.send :include, InstanceMethods
base.send :alias_method_chain, :belongs_to, :polymorphism # This
breaks everything!
end
module ClassMethods
def polymorphic(class_or_str, record_id = nil)
i = t = nil
if class_or_str.is_a?(String) and record_id.nil?
i, t = class_or_str.split(’:’) rescue [nil, nil]
raise ArgumentError, “Invalid polymorphic object reference
string (bad format): #{class_or_str}” unless i.is_a?(String) and
t.is_a?(String)
i = i.to_i rescue nil
t = eval(t) rescue nil
elsif class_or_string.is_a?(String) and record_id.is_a?
(Integer)
i = record_id
t = eval(class_or_string) rescue nil
elsif class_or_string.is_a?(Class) and record_id.is_a?
(Integer)
i = record_id
t = class_or_string
else
raise ArgumentError, “Invalid parameter types: (#
{class_or_str.class}, #{record_id.class})”
end
raise ArgumentError, “Invalid value for object id in
polymorphic reference string: #{class_or_str}” unless i
raise ArgumentError, “Invalid polymorphic type name: #
{class_or_string}” unless t
raise ArgumentError, “Invalid object reference (bad values):
[type=#{t}] [id=#{i}]” unless i > 0 and t.respond_to?
(:descends_from_active_record?) and t != ::ActiveRecord::Base
t.base_class.find(i)
end
def belongs_to_with_polymorphism(association_id, options = {})
add_poly = options[:polymorphic]
belongs_to_without_polymorphism(association_id, options)
if add_poly
string_reader = “#{association_id}_string”
string_writer = “#{association_id}_string=”
object_reader = association_id.dup
object_writer = “#{association_id}=”
define_method(string_reader) do
ref = self.send(object_reader)
ref.polymorphic rescue nil
end
define_method(string_writer) do |reference|
obj = self.class.polymorphic(reference) if reference
self.send(object_writer, obj)
end
end
end
end
module InstanceMethods
def polymorphic
return nil if new_record?
“#{id}:#{self.class.base_class}”
end
end
end
end
ActiveRecord::Base.send :include, PolymorphicReferences::ActiveRecord