Wisper::ActiveTracker
Transparently publish model lifecycle events to subscribers. Using Wisper events is a better alternative to ActiveRecord callbacks and Observers. Listeners are subscribed to models at runtime.
Installation
gem 'wisper-active_tracker'
Usage
Setup a publisher
class Meeting < ActiveRecord::Base
include Wisper::ActiveTracker
# ...
end
If you wish all models to broadcast events without having to explicitly include
Wisper::ActiveTracker
add the following to an initializer:
Wisper::ActiveTracker.extend_all
Subscribing
Subscribe a listener to model instances:
meeting = Meeting.new
meeting.subscribe(Auditor.new)
Subscribe a block to model instances:
meeting.on(:create_meeting_successful) { |meeting_id, changes| ... }
Subscribe a listener to all instances of a model:
Meeting.subscribe(Auditor.new)
Please refer to the Wisper README for full details about subscribing.
The events which are automatically broadcast are:
after_create
create_<model_name>_{successful, failed}
after_update
update_<model_name>_{successful, failed}
before_create
after_destroy
destroy_<model_name>_successful
after_commit
<model_name>_committed
after_rollback
Reacting to Events
To receive an event the listener must implement a method matching the name of the event with a single argument, the instance of the model.
def create_meeting_successful(meeting_id, changes)
# ...
end
Example
Controller
class MeetingsController < ApplicationController
def new
@meeting = Meeting.new
end
def create
@meeting = Meeting.new(params[:meeting])
@meeting.subscribe(Auditor.instance)
@meeting.on(:meeting_create_successful) { redirect_to meeting_path }
@meeting.on(:meeting_create_failed) { render action: :new }
@meeting.save
end
def edit
@meeting = Meeting.find(params[:id])
end
def update
@meeting = Meeting.find(params[:id])
@meeting.subscribe(Auditor.instance)
@meeting.on(:meeting_update_successful) { redirect_to meeting_path }
@meeting.on(:meeting_update_failed) { render :action => :edit }
@meeting.update_attributes(params[:meeting])
end
end
Using on
to subscribe a block to handle the response is optional,
you can still use if @meeting.save
if you prefer.
Listener
Which simply records an audit in memory
class Auditor
include Singleton
attr_accessor :audit
def initialize
@audit = []
end
def after_create(subject_id, changes)
push_audit_for('create', subject)
end
def after_update(subject_id, changes)
push_audit_for('update', subject)
end
def after_destroy(subject_id, changes)
push_audit_for('destroy', subject)
end
def self.audit
instance.audit
end
private
def push_audit_for(action, subject)
audit.push(audit_for(action, subject))
end
def audit_for(action, subject)
{
action: action,
subject_id: subject.id,
subject_class: subject.class.to_s,
changes: subject.previous_changes,
created_at: Time.now
}
end
end
Do some CRUD
Meeting.create(:description => 'Team Retrospective', :starts_at => Time.now + 2.days)
meeting = Meeting.find(1)
meeting.starts_at = Time.now + 2.months
meeting.save
And check the audit
Auditor.audit # => [...]