No commit activity in last 3 years
No release in over 3 years
Subscribe to changes on ActiveRecord models
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 3.0.0
~> 2.0
 Project Readme

Wisper::ActiveRecord

Gem Version Code Climate Build Status

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-activerecord'

Usage

Setup a publisher

class Meeting < ActiveRecord::Base
  include Wisper.model

  # ...
end

If you wish all models to broadcast events without having to explicitly include Wisper.model add the following to an initializer:

Wisper::ActiveRecord.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| ... }

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
  • after_update
  • after_destroy
  • create_<model_name>_{successful, failed}
  • update_<model_name>_{successful, failed}
  • destroy_<model_name>_successful
  • <model_name>_committed
  • after_commit
  • 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)
  # ...
end

Example

Controller

class MeetingsController < ApplicationController
  def new
    @meeting = Meeting.new
  end

  def create
    @meeting = Meeting.new(params[:meeting])
    @meeting.subscribe(Auditor.new)
    @meeting.on(:create_meeting_successful) { redirect_to meeting_path }
    @meeting.on(:create_meeting_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.new)
    @meeting.on(:update_meeting_successful) { redirect_to meeting_path }
    @meeting.on(:update_meeting_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

  def after_create(subject)
    push_audit_for('create', subject)
  end

  def after_update(subject)
    push_audit_for('update', subject)
  end

  def after_destroy(subject)
    push_audit_for('destroy', subject)
  end

  def self.audit
    @audit ||= []
  end

  private

  def push_audit_for(action, subject)
    self.class.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 # => [...]

Notes on Testing

ActiveRecord <= 4.0

This gem makes use of ActiveRecord's after_commit lifecycle hook to broadcast events, which will create issues when testing with transactional fixtures. Unless you also include the test_after_commit gem ActiveRecord models will not broadcast any lifecycle events within your tests.

Compatibility

Tested on CRuby, Rubinius and JRuby for ActiveRecord ~> 3.0, ~> 4.0, and ~> 5.0.

See the CI build status for more information.

Contributing

Please submit a Pull Request with specs.

Running the specs

bundle exec rspec