Forum: Ruby on Rails Overriding the belongs_to macro method

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.
Kendall G. (Guest)
on 2009-05-20 20:30
I want to extend the functionality of ActiveRecord's `belongs_to' macro
method so that, in addition to performing its normal tasks and creating
the default association methods, I can have it also create a couple
extra *custom* association methods. I'll post my code of how I tried
(and failed) to do this below. Anyone know if/how this can be done?

Specifically, what I'm doing is trying to have `belongs_to' add two more
methods to an AR class *if* the association is polymorphic
(options[:polymorphic] == true). If the association is polymorphic, I
try to add the following two custom association methods:

__CODE__
def association_string
  "#{association.id}:#{association.class.base_class}"
end

def association_string=(ref)
  i, t = ref.split(':')
  association = eval(t).find(i.to_i)
end
__CODE__

I try to override belongs_to by using `alias_method_chain' so I can call
the original implementation, followed by my own logic. The code is in a
file in my `config/initializers' directory so it will get picked up when
the rails env loads.

The error I get is: `alias_method':NameError: undefined method
`belongs_to' for class `ActiveRecord::Base'

Indeed, if I load a script/console session and do:

ActiveRecord::Base.methods.find {|m| m =~ /belongs_to/}

I get no match for `belongs_to' (though `has_and_belongs_to_many' *is*
found interestingly enough).

I see that in the docs `belongs_to' is in module
ActiveRecord::Associations::ClassMethods which, I'd assume, gets
included (or extended) into AR::Base, making the method available.
Either way, the method I need to override isn't defined/part-of
AR::Base. Anyone know how/why/what/where or how to better accomplish the
above?

Here's the full code listing of what I'm trying to do:

FILE: config/initializers/polymorphic_references.rb

==========CODE==========

module PolymorphicReferences
  ##########
  #
  # Extensions to ActiveRecord
  #
  module ActiveRecord
    def self.included(base)
      base.send :extend,  ClassMethods
      base.send :include, InstanceMethods
      base.send :alias_method_chain, :belongs_to, :polymorphism
    end
    ##########
    #
    # ActiveRecord::Base class methods
    #
    module ClassMethods
      #
      # Load object polymorphically
      #
      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, "...message..." unless
              i.is_a?(String) and t.is_a?(String)
          i = i.to_i  rescue nil
          t = eval(t) rescue nil
        elsif class_or_str.is_a?(String) and
              record_id.is_a?(Integer)
          i = record_id
          t = eval(class_or_str) rescue nil
        elsif class_or_str.is_a?(Class) and
              record_id.is_a?(Integer)
          i = record_id
          t = class_or_str
        else
          raise ArgumentError, "...message..."
        end
        raise ArgumentError, "...message..." unless i
        raise ArgumentError, "...message..." unless t
        raise ArgumentError, "...message..." unless i > 0 and
            t.respond_to?(:descends_from_active_record?) and
            t != ::ActiveRecord::Base
        t.base_class.find(i)
      end
      #
      # Hook into belongs_to to add association_string methods:
      #
      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
    ##########
    #
    # ActiveRecord::Base instance methods
    #
    module InstanceMethods
      #
      # This returns a polymorphic "reference" string.
      #
      def polymorphic
        return nil if new_record?
        "#{id}:#{self.class.base_class}"
      end
    end
  end
end
#
##########
#
# Plug the system into rails
#
ActiveRecord::Base.send :include, PolymorphicReferences::ActiveRecord

========== END OF CODE ==========

Thanks in advance for any help/responses/insights/condolences.
Kendall G. (Guest)
on 2009-05-21 01:43
So, I'm posting a reply to my own original post. I fixed my problem and
thought I'd follow up for posterity.

The pertinent broken code was:

 def self.included(base)
   base.send :extend,  ClassMethods
   base.send :include, InstanceMethods
   base.send :alias_method_chain, :belongs_to, :polymorphism
 end

The fixed code is:

 def self.included(base)
   base.send :extend,  ClassMethods
   base.send :include, InstanceMethods
   class << base
     alias_method_chain :belongs_to, :polymorphism
   end
 end

Calling alias_method_chain (or just alias_method for that matter) on the
class from "outside" (aka by klass.send :alias_method, ...) runs in the
wrong context. It wants to alias regular instance methods of the klass.
Instead, alias_method needs to happen in (still don't know proper ruby
terms for this stuff, sorry) *class* scope (???) so that it will look
for class methods to alias.

As an aside, I was quite retarded when, in my previous post, I ran the
following command in an attempt to see if the belongs_to method was in
fact defined:

ActiveRecord::Base.methods.find {|m| m =~ /belongs_to/}

If, of course, instead finds the first method that matches which was
`has_and_[i]belongs_to[/i]_many' in my case. I should've been using
[i]find_all[/i] instead. Well, yeah, just an FYI.

I originally wrote:
> http://www.ruby-forum.com/topic/187419
> (there, just saved a ton of space and a whole lotta money on
> my ___ _________ by switching to _____)
This topic is locked and can not be replied to.