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