I want to intercept method calls on a ruby-class and being able to do something before and after the actual execution of the method. I tried the following code, but get the error:
MethodInterception.rb:16:in
before_filter': (eval):2:inalias_method': undefined methodsay_hello' for classHomeWork' (NameError) from (eval):2:in `before_filter'
Can anybody help me to do it right?
class MethodInterception
  def self.before_filter(method)
    puts "before filter called"
    method = method.to_s
    eval_string = "
      alias_method :old_#{method}, :#{method}
      def #{method}(*args)
        puts 'going to call former method'
        old_#{method}(*args)
        puts 'former method called'
      end
    "
    puts "going to call #{eval_string}"
    eval(eval_string)
    puts "return"
  end
end
class HomeWork < MethodInterception
  before_filter(:say_hello)
  def say_hello
    puts "say hello"
  end
end
                I just came up with this:
module MethodInterception
  def method_added(meth)
    return unless (@intercepted_methods ||= []).include?(meth) && !@recursing
    @recursing = true # protect against infinite recursion
    old_meth = instance_method(meth)
    define_method(meth) do |*args, &block|
      puts 'before'
      old_meth.bind(self).call(*args, &block)
      puts 'after'
    end
    @recursing = nil
  end
  def before_filter(meth)
    (@intercepted_methods ||= []) << meth
  end
end
Use it like so:
class HomeWork
  extend MethodInterception
  before_filter(:say_hello)
  def say_hello
    puts "say hello"
  end
end
Works:
HomeWork.new.say_hello
# before
# say hello
# after
The basic problem in your code was that you renamed the method in your before_filter method, but then in your client code, you called before_filter before the method was actually defined, thus resulting in an attempt to rename a method which doesn't exist.
The solution is simple: Don't Do That™!
Well, okay, maybe not so simple. You could simply force your clients to always call before_filter after they have defined their methods. However, that is bad API design.
So, you have to somehow arrange for your code to defer the wrapping of the method until it actually exists. And that's what I did: instead of redefining the method inside the before_filter method, I only record the fact that it is to be redefined later. Then, I do the actual redefining in the method_added hook.
There is a tiny problem in this, because if you add a method inside of method_added, then of course it will immediately get called again and add the method again, which will lead to it being called again, and so on. So, I need to guard against recursion.
Note that this solution actually also enforces an ordering on the client: while the OP's version only works if you call before_filter after defining the method, my version only works if you call it before. However, it is trivially easy to extend so that it doen't suffer from that problem.
Note also that I made some additional changes that are unrelated to the problem, but that I think are more Rubyish:
Module#define_method instead of eval: eval is evil. 'Nuff said. (There was absolutely no reason whatsoever to use eval in the first place, in the OP's code.)alias_method: the alias_method chain technique pollutes the namespace with useless old_foo and old_bar methods. I like my namespaces clean.I just fixed some of the limitations I mentioned above, and added a few more features, but am too lazy to rewrite my explanations, so I repost the modified version here:
module MethodInterception
  def before_filter(*meths)
    return @wrap_next_method = true if meths.empty?
    meths.delete_if {|meth| wrap(meth) if method_defined?(meth) }
    @intercepted_methods += meths
  end
  private
  def wrap(meth)
    old_meth = instance_method(meth)
    define_method(meth) do |*args, &block|
      puts 'before'
      old_meth.bind(self).(*args, &block)
      puts 'after'
    end
  end
  def method_added(meth)
    return super unless @intercepted_methods.include?(meth) || @wrap_next_method
    return super if @recursing == meth
    @recursing = meth # protect against infinite recursion
    wrap(meth)
    @recursing = nil
    @wrap_next_method = false
    super
  end
  def self.extended(klass)
    klass.instance_variable_set(:@intercepted_methods, [])
    klass.instance_variable_set(:@recursing, false)
    klass.instance_variable_set(:@wrap_next_method, false)
  end
end
class HomeWork
  extend MethodInterception
  def say_hello
    puts 'say hello'
  end
  before_filter(:say_hello, :say_goodbye)
  def say_goodbye
    puts 'say goodbye'
  end
  before_filter
  def say_ahh
    puts 'ahh'
  end
end
(h = HomeWork.new).say_hello
h.say_goodbye
h.say_ahh
                        Less code was changed from original. I modified only 2 line.
class MethodInterception
  def self.before_filter(method)
    puts "before filter called"
    method = method.to_s
    eval_string = "
      alias_method :old_#{method}, :#{method}
      def #{method}(*args)
        puts 'going to call former method'
        old_#{method}(*args)
        puts 'former method called'
      end
    "
    puts "going to call #{eval_string}"
    class_eval(eval_string) # <= modified
    puts "return"
  end
end
class HomeWork < MethodInterception
  def say_hello
    puts "say hello"
  end
  before_filter(:say_hello) # <= change the called order
end
This works well.
HomeWork.new.say_hello
#=> going to call former method
#=> say hello
#=> former method called
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With