ruby-on-railsactiverecordcallbackhas-many-through

Dynamically create after_add and after_remove callbacks for has_many or habtm?


Is there a way to dynamically add after_add and after_remove callbacks to an existing has_many or has_and_belongs_to_many relationship?

For example, suppose I have models User, Thing, and a join model UserThingRelationship, and the User model is something like this:

class User < ActiveRecord::Base
  has_many :user_thing_relationships
  has_many :things, :through => :user_thing_relationships
end

I'd like to be able to, in a module that extends User, add :after_add and :after_remove callbacks to the User.has_many(:things, ...) relationship. I.e., have something like

module DoesAwesomeStuff
  def does_awesome_stuff relationship, callback
    # or however this can be achieved...
    after_add(relationship) callback
    after_remove(relationship) callback
  end
end

So that

class User < ActiveRecord::Base
  has_many :user_thing_relationships
  has_many :things, :through => :user_thing_relationships

  does_awesome_stuff :things, :my_callback
  def my_callback; puts "awesome"; end
end

Is effectively the same as

class User < ActiveRecord::Base
  has_many :user_thing_relationships
  has_many :things, :through => :user_thing_relationships, :after_add => :my_callback, :after_remove => :my_callback

  def my_callback; puts "awesome"; end
end

This can be done pretty effectively for adding after_save, etc, callbacks to the model that's being extended, since ActiveRecord::Base#after_save is just a class method.


Solution

  • I was able to come up with the following by using ActiveRecord::Reflection:

    module AfterAdd
      def after_add rel, callback
        a = reflect_on_association(rel)
        send(a.macro, rel, a.options.merge(:after_add => callback))
      end
    end
    
    class User < ActiveRecord::Base
      extend AfterAdd
    
      has_many :user_thing_relationships
      has_many :things, :through => :user_thing_relationships
    
      after_add :things, :my_callback
    
      def my_callback
        puts "Hello"
      end
    end
    

    I don't want to answer my own question, so I won't give myself answer credit if someone else can come up with a better solution in the next few days.