Forum: Ruby on Rails Polymorphism and belongs_to: trying to override belongs_to

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
94cee02d73877ef5e6dfb04afb1fe324?d=identicon&s=25 Kendall (Guest)
on 2009-05-19 23:17
(Received via mailing list)
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
This topic is locked and can not be replied to.