How to Implement a "Callback" in Ruby

How to implement a callback in Ruby?

The ruby equivalent, which isn't idiomatic, would be:

def my_callback(a, b, c, status_code)
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
end

def do_stuff(a, b, c, callback)
sum = a + b + c
callback.call(a, b, c, sum)
end

def main
a = 1
b = 2
c = 3
do_stuff(a, b, c, method(:my_callback))
end

The idiomatic approach would be to pass a block instead of a reference to a method. One advantage a block has over a freestanding method is context - a block is a closure, so it can refer to variables from the scope in which it was declared. This cuts down on the number of parameters do_stuff needs to pass to the callback. For instance:

def do_stuff(a, b, c, &block)
sum = a + b + c
yield sum
end

def main
a = 1
b = 2
c = 3
do_stuff(a, b, c) { |status_code|
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
}
end

Using method callbacks in plain Ruby class

A naive implementation would be;

module Callbacks

def self.extended(base)
base.send(:include, InstanceMethods)
end

def overridden_methods
@overridden_methods ||= []
end

def callbacks
@callbacks ||= Hash.new { |hash, key| hash[key] = [] }
end

def method_added(method_name)
return if should_override?(method_name)

overridden_methods << method_name
original_method_name = "original_#{method_name}"
alias_method(original_method_name, method_name)

define_method(method_name) do |*args|
run_callbacks_for(method_name)
send(original_method_name, *args)
end
end

def should_override?(method_name)
overridden_methods.include?(method_name) || method_name =~ /original_/
end

def before_run(method_name, callback)
callbacks[method_name] << callback
end

module InstanceMethods
def run_callbacks_for(method_name)
self.class.callbacks[method_name].to_a.each do |callback|
send(callback)
end
end
end
end

class Foo
extend Callbacks

before_run :bar, :zoo

def bar
puts 'bar'
end

def zoo
puts 'This runs everytime you call `bar`'
end

end

Foo.new.bar #=> This runs everytime you call `bar`
#=> bar

The tricky point in this implementation is, method_added. Whenever a method gets bind, method_added method gets called by ruby with the name of the method. Inside of this method, what I am doing is just name mangling and overriding the original method with the new one which first runs the callbacks then calls the original method.

Note that, this implementation neither supports block callbacks nor callbacks for super class methods. Both of them could be implemented easily though.

How to properly make a callback to create a model, after initialize of another model?

As prasannaboga already wrote in his comment. Callbacks like these are running in the context of an instance. That means you do not need to call a specific user first because the current object is already that user.

Additionally, in the context of a model, the redirect_to call doesn't make any sense. Models don't know anything about requests and responses. redirect_to is a controller method and you need to add it to your controller.

The following callback method should work:

def create_account
Account.create(owner_id: id, name: "#{name}'s Account")
end

I wonder why you decided to not add an has_one :account or has_many :accounts association to your User model? By doing so you could simplify the method even more because then Rails would handle setting the id automatically, like this

build_account(name: "#{name}'s Account")  # when `has_one` or
accounts.build(name: "#{name}'s Account") # when `has_many`

Define custom callbacks on ruby method

I would do something like this:

require 'active_support'
class Base
include ActiveSupport::Callbacks
define_callbacks :notifier

set_callback :notifier, :after do |object|
notify()
end

def notify
puts "notified successfully"
end

def call
run_callbacks :notifier do
do_call
end
end

def do_call
raise 'this should be implemented in children classes'
end
end

class NewPost < Base
def do_call
puts "Creating new post on WordPress"
end
end

person = NewPost.new
person.call

Another solution without ActiveSupport:

module Notifier
def call
super
puts "notified successfully"
end
end


class NewPost
prepend Notifier

def call
puts "Creating new post on WordPress"
end
end

NewPost.new.call

You should check your ruby version prepend is a "new" method (2.0)

How to create a common after_create callback for all models?

You can have a GenericModule and include this in any model you wish

module GenericModule
extend ActiveSupport::Concern

included do
after_create :log_creation
end

def log_creation
# perform logging
end
end

And in the model

class User < ActiveRecord::Base
include GenericModule

# ...other model code...
end

You can have this for all your models in which you need this behavior.

Can you manually trigger a callback in Ruby on Rails?

If you're happy to run both :before and :after hooks, you can try run_callbacks.
From the docs:

run_callbacks(kind, &block)

Runs the callbacks for the given event.

Calls the before and around callbacks in the order they were set, yields the block (if given one), and then runs the after callbacks in reverse order.

If the callback chain was halted, returns false. Otherwise returns the result of the block, or true if no block is given.

run_callbacks :save do
save
end

Custom Hook/Callback/Macro Methods

Here's a solution that uses prepend. When you call before_operations for the first time it creates a new (empty) module and prepends it to your class. This means that when you call method foo on your class, it will look first for that method in the module.

The before_operations method then defines simple methods in this module that first invoke your 'before' method, and then use super to invoke the real implementation in your class.

class ActiveClass
def self.before_operations(before_method,*methods)
prepend( @active_wrapper=Module.new ) unless @active_wrapper
methods.each do |method_name|
@active_wrapper.send(:define_method,method_name) do |*args,&block|
send before_method
super(*args,&block)
end
end
end
end

class SubClass < ActiveClass
before_operations :first_validate_something, :do_this_method, :do_that_method

def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end

private

def first_validate_something
p :validating
end
end

SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}

If you want to make the idea by @SteveTurczyn work you must:

  1. receive the args params in the block of define_method, not as arguments to it.
  2. call before_operations AFTER your methods have been defined if you want to be able to alias them.

 

class ActiveClass
def self.before_operations(before_method, *methods)
methods.each do |meth|
raise "No method `#{meth}` defined in #{self}" unless method_defined?(meth)
orig_method = "_original_#{meth}"
alias_method orig_method, meth
define_method(meth) do |*args,&block|
send before_method
send orig_method, *args, &block
end
end
end
end

class SubClass < ActiveClass
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end

before_operations :first_validate_something, :do_this_method, :do_that_method

private
def first_validate_something
p :validating
end
end

SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}


Related Topics



Leave a reply



Submit