0

Basically, this is normal code:

class Foo
   def hi
      # your code here....
   rescue => e
      # Raise error here
   end

   def hello
      # your code here...
   rescue => e
      # Raise error here
   end
end

But in PHP, i can use __call magic method to create abstract class, look like this:

class FooAbstract {
    public function __call($name, $args) {
       # Try catch in here...
    }
}

class Foo extends FooAbstract {
   public function hi() {
     # Code with try catch...
   }
}

How can i use __call method in Ruby class???

Kimmo Lehto
  • 5,910
  • 1
  • 23
  • 32
Dean
  • 415
  • 1
  • 5
  • 15
  • If you want to define a class, which can not be instantiated except if subclassed, you could do a `fail "Trying to instantiate abstract class" if self.class == FooAbstract` in FooAbstract#initialize. – user1934428 Jun 18 '18 at 09:31
  • 3
    This looks like a XY problem. What are you trying to accomplish? – Stefan Jun 18 '18 at 09:47
  • There is no *direct* equivalent to `__call` in ruby; the closest is probably `method_missing`. (And likewise, there's no *direct* equivalent to `method_missing` in PHP, but as far as I know `__call` is probably the closest.) But as Stefan says, what are you actually trying to achieve here? I can't really advise how to structure the code without knowing what problem you're trying to solve. – Tom Lord Jun 18 '18 at 10:21
  • My guess is to accomplish something like the rails class level `rescue_from` so that you don't have to copy-paste the same `rescue` block to every method. – Kimmo Lehto Jun 18 '18 at 11:52
  • It's also possible to do something this globally by defining `at_exit { Logger.new('/tmp/log').error($!) if $! }` if you for example just want to write out a log of all the exceptions. But this is global, not class level. – Kimmo Lehto Jun 18 '18 at 11:53
  • 1
    Not sure what you're trying to illustrate with that PHP code, but there method `hi` will not invoke `__call` in any way. What was `__call` supposed to do, add exception handling for all methods of child classes? – Sergio Tulentsev Jun 18 '18 at 11:58
  • You could create a single interface for all method calls and wrap that e.g. `def call(method_name,*args,&block); begin;public_send(method_name,*args,&block);rescue;end;end`. Then `Foo.new.(:hi)` or `Foo.new.call(:hello)` However calling around this like `Foo.new.hi` would escape the handling – engineersmnky Jun 18 '18 at 19:09

2 Answers2

5

You could define a module that when included defines a method_added hook that wraps all new methods inside a begin..rescue block:

require 'set'

module ExceptionHandler

  def self.included(klass)
    super
    klass.send(:extend, ClassMethods)
  end

  module ClassMethods
    def exception_handler(&block)
      @__exception_handler = block.to_proc
    end

    def handle_exception(exception)
      defined?(@__exception_handler) ? @__exception_handler.call(exception) : super
    end

    def handle_method_exceptions(method_name)
      old_method = instance_method(method_name)
      return if (@__hooked_methods ||= Set.new).include?(method_name)

      @__ignoring_added_methods = true # avoid infinite define_method/method_added loop
      define_method method_name do |*args, &block|
        begin
          old_method.bind(self).(*args, &block)
        rescue => ex
          self.class.handle_exception(ex)
        end
      end
      @__ignoring_added_methods = false

      @__hooked_methods << method_name
    end

    def method_added(method_name)
      super
      unless @__ignoring_added_methods
        handle_method_exceptions(method_name)
      end
    end
  end
end

This would be used like:

class Foo
  include ExceptionHandler

  exception_handler do |exception|
    puts "Catched an exception:"
    puts "---------------------"
    puts "Exception class: #{exception.class}"
    puts "Message: #{exception.message}"
    puts "Backtrace:"
    puts exception.backtrace.join("\n  ")
    puts
    puts "reraising.."
    puts
    raise exception
  end

  def this_raises
    raise "somebody set up us the bomb"
  end
end

Foo.new.this_raises

This would output:

Catched an exception:
---------------------
Exception class: RuntimeError
Message: somebody set up us the bomb
Backtrace:
errorhandler.rb:62:in `this_raises'
  errorhandler.rb:26:in `call'
  errorhandler.rb:26:in `block in handle_exceptions'
  errorhandler.rb:67:in `<main>'

reraising..

I'm not sure if it is a good idea.

You could take out the method_added part and it would look something like:

class Foo
  with_rescue def foofoo(arg)
    puts arg.inspect
  end
end

(You can just rename the handle_method_exceptions to with_rescue and remove all the @__ignoring_added_methods trickery and the method_added method and it should work as described).

Kimmo Lehto
  • 5,910
  • 1
  • 23
  • 32
  • This is great! But how is it possible to use an instance method as the `exception_handler`? I want to be able to log an exception in any method to my instance specific logger. – meesern Sep 10 '19 at 09:18
  • @meesern untested, but you can try to replace `self.class.handle_exception(ex)` with `(respond_to?(:handle_exception) ? self : self.class).send(:handle_exception, ex)` and define the exception handler as an instance method: `def handle_exception(ex) ... end` – Kimmo Lehto Sep 11 '19 at 07:34
1

I'm not sure what you want to achieve here, but the Ruby equivalent of PHP's __call() is method_missing.

By default, when you try to call a non-existing method you will get an exception. But if you want to implement an "abstract class". You could also try this solution: https://stackoverflow.com/a/512505/185870

Teoulas
  • 2,943
  • 22
  • 27