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 # => [...]