Introducing ActiveModelListener: Easy to use global ActiveRecord event listeners

September 27, 2009 Pivotal Labs

I’m currently working on a large app where certain things have to happen when records are created, updated and deleted, such as:

  • Publishing to an activity feed
  • Generating emails
  • Adding entries to a changelog
  • Generating tasks and reminders

Further, the requirements state that admin users should be able to configure which of these actions happen for which objects in the system, who they go to, what the text is etc…

At first this looks like a great place for ActiveRecord Observers. However, after working with Observers there are a few things I dislike – namely that you can’t easily apply observers to all of your models, and you can’t selectively turn them on and off in tests. To remedy that problem, I created ActiveModelListener.

ActiveModelListener is a simple, global ActiveRecord event listener framework, using a middleware-esque architecture that can easily be turned on and off.

Installation

sudo gem install gemcutter
sudo gem tumble
sudo gem install active_model_listener

Usage

First, require active_model_listener above your rails initializer in environment.rb:

# environment.rb
require 'active_model_listener'
Rails::Initializer.run do |config|
  # ...
end

Next, add the listeners you’d like to apply (in order) to the ActiveModelListener in an initializer:

# config/initializers/active_model_listener.rb
ActiveModelListener.listeners << ActivityFeedListener

Then, create a listener class that defines methods for after_create, after_update and / or after_destroy, like so:

class ActivityFeedListener
  class << self
    def after_create(record)
      description = "#{record.class.name} was created"
      publish_activity_feed_items record, description
    end

    def after_update(record)
      description = "#{record.class.name} was updated"
      publish_activity_feed_items record, description
    end

    def after_destroy(record)
      description = "#{record.class.name} was deleted"
      publish_activity_feed_items record, description
    end

    def publish_activity_feed_items(record, description)
      record.activity_feed_item_subscribers.each do |subscriber|
        ActivityFeedItem.create :user => subscriber, :description => description
      end
    end

    private :publish_activity_feed_items
  end
end

Notice how the class looks almost identical to an ActiveRecord observer, so you can easily refactor between the two.

Turning off listeners in specs

When unit testing if your listeners are all firing your unit tests become integration tests. To avoid this, you can easily turn off listeners for all specs all the time:

Spec::Runner.configure do |config|
  config.before(:each) do
    ActiveModelListener.listeners.clear
  end
end

Then, when you want them back on again, you can turn them back on for a spec:

describe "Integrating with listeners" do
  before do
    ActiveModelListener.listeners << FooListener
  end
end

Specifying a subset of listeners to use

When doing data imports, migrations or certain actions that need to only use certain listeners, you can easily specify which ones you’d like to use:

ActiveModelListener.with_listeners AuditListener, ActivityListener do
  Article.create! :title => "foo"
end

After the block runs, the original listeners are restored.

If you want to run some code with no listeners at all, you can do so with:

ActiveModelListener.without_listeners do
  Article.create! :title => "foo"
end

Contributing

http://github.com/zilkey/active_model_listener

About the Author

Biography

Previous
svn to git protips
svn to git protips

When you're moving a codebase from subversion to git, here are a few things that make the move go a bit mor...

Next
Remixr: Ruby wrapper for the Best Buy Remix API
Remixr: Ruby wrapper for the Best Buy Remix API

sudo gem install remixr We at Pivotal like that incantation. Thanks to the Squeegee crew for putting Rem...