Rails

Fun with ActiveRecord::Observer


Disclaimer

ActiveRecord::Observer will be extracted to a separated gem when Rails 4 is released, so if you are planning to upgrade to the latest Rails version and want to use ActiveRecord::Observer, you'll need to add the rails-observer gem to your Gemfile.

Using ActiveRecord::Observer is a good practice, instead of bloating your model with a lot of callbacks, moving this behavior to another class with only this responsability sounds great, but ActiveRecord::Observer has some "limitations", I quoted that because they seem like limitations, but the problem is that ActiveRecord::Observer's capabilities aren't fully documented or aren't visible, you have to dig in the source code and figure out how it works.

By default, ActiveRecord::Observer watched a limited set of callbacks defined by the ActiveRecord::Callbacks::CALLBACKS constant, which are:

From the rails source

CALLBACKS = [
  :after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
  :before_save, :around_save, :after_save, :before_create, :around_create,
  :after_create, :before_update, :around_update, :after_update,
  :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]

But what if you want to add you own callbacks?, is there a way to do that? There surely is one.

Adding your own callbacks

ActiveRecord::Observer functionality is fairly easy to understand, there is a notify_observers method in the ActiveModel::Observing module that we can use to define our own callbacks with something like this:

module Notifications
  def notify_completed
    notify_observers :after_complete
  end
end

class Action < ActiveRecord::Base
  include Notifications

  def complete!
    ...
    notify_completed
  end
end

class ActionObserver < ActiveRecord::Observer
  def after_complete(action)
    # your callback logic here
    ...
  end
end

And that's it, you'll have the after_complete callback available in your observer class, this method will receive the model instance where the callback is triggered as an argument.

But, what if I want to know more about the event that triggered the callback, not just the instance where it ocurred? Maybe I want to find out more about the user that triggered the action, is there a way to do that?, well, try this.

Sending more arguments in the callback

If you look in the notify_observers source, you'll see that it only receives one argument at a time, but you can see that this method uses a class method with the same name but this one accepts many arguments.

In the original notify_observers they always use self as the second argument, we are going to change that with another object, I'll use a hash, but you can wrap your arguments into a class, the important thing here is that the second argument needs to be an object.

module Notifications
  def notify_completed(user)
    self.class.notify_observers :after_complete, { action: self, user: user }
  end
end

class Action < ActiveRecord::Base
  include Notifications

  def complete!(user)
    ...
    notify_completed(user)
  end
end

class YourModelObserver < ActiveRecord::Observer
  def after_complete(event)
    # your callback logic here
        # The event argument will be a Hash with action and user keys.
    ...
  end
end

And that's it.

There are many things that Rails can do for you, but you'll need to go beyond the docs, diving deep in the source is the best way to find those things.

Hope you find this useful, thanks for reading!

Community
De Código, Café y Cervezas 06 – ActiveModel::Serializer
Rails
Active Job
Craftsmanship
De Código, Café y Cervezas 08 – Web Services