A Guide to Solidus Events: Everyone's Invited!

This post was originally posted on the Nebulab website here.

Event programming (also known as the pub/sub pattern) has been gaining lot of traction in recent times, not without reason. Events allow to easily add custom behavior to a process that the developer may not own directly or know in detail: you just need to subscribe to a known event and then, when it happens, your code will run.

In the past, a few attempts were made to add an events system to Solidus, and finally events became a reality in version 2.9 after this PR.

Solidus events: the basics

How do events work in Solidus? They're actually rather simple, let's start the Solidus sandbox console and get our hands dirty. First, we need to subscribe to the event we're insterested in:

  Spree::Event.subscribe 'foo' do |event|
    puts "Event with name '#{event.name}' just fired!"
  end

then, we can manually fire it:

  Spree::Event.fire :foo

and the text Event with name 'foo' just fired! will appear on your screen... that was easy 🍰

Introducing event subscribers

Let's now consider a more ‘real life’ scenario. We want to deliver transactional emails to customers in a centralized way.

The first step is to patch the state machines of some Solidus models in order to fire events when the shipment or order state changes. These two modules will do the trick:

  module OrderDecorator
    def self.prepended(base)
      base.state_machine do
        after_transition do |order, transition|
          Spree::Event.fire "order_#{transition.to_name}", order: order
        end
      end
    end

    Spree::Order.prepend(self)
  end

  module ShipmentDecorator
    def self.prepended(base)
      base.state_machine do
        after_transition do |shipment, transition|
          Spree::Event.fire "shipment_#{transition.to_name}", shipment: shipment
        end
      end
    end

    Spree::Shipment.prepend(self)
  end

Then, we add a subscriber module that will deliver mails when orders and shipments change to the right state:

  module TransactionalEmailsSubscriber
    include Spree::Event::Subscriber

    event_action :order_complete
    event_action :order_canceled
    event_action :shipment_shipped

    def order_complete(event)
      TransactionalMailer.notify_order(event.payload[:order])
    end

    def order_canceled(event)
      TransactionalMailer.notify_order(event.payload[:order])
    end

    def shipment_shipped(event)
      TransactionalMailer.notify_shipment(event.payload[:shipment])
    end

    subscribe!
  end

The module includes Spree::Event::Subscriber, which simplifies events subscriptions by sparing you to write some tedious boilerplate.

You need to define which event you want to subscribe to with the event_action macro and then define a corresponding method that will be called when the event is triggered.

If the module is loaded late enough in the initialization process of the app, then it can be subscribed to directly from within its definition (see last line), otherwise it needs to be explicitly subscribed to, for example in an initializer:

  TransactionalEmailsSubscriber.subscribe!

The last and optional step, if you want to actually see the examples work, is to define the fake TransactionalMailer class:

  module TransactionalMailer
    extend self

    def notify_order(order)
      puts "Dear #{customer(order)}, your order #{order.number} is now #{order.state}."
    end

    def notify_shipment(shipment)
      order = shipment.order
      puts "Dear #{customer(order)}, shipment with tracking #{shipment.tracking} for order #{order.number} is now #{shipment.state}."
    end

    private

    def customer(order)
      order.bill_address.firstname
    end
  end

Everything is now in place, so you just need to complete, cancel or ship some shipments. One quick way is to copy all the code above in the Solidus core specs for the order or shipment model, run them and watch the text appear on screen:

Solidus shipments test run in console

Subscribe to multiple events

Recently Solidus gained the ability to subscribe to multiple events by using regular expressions. This is a feature made possible by the fact that ActiveSupportNotifications supports regexps — though other event adapters, when they will become available, may not offer this feature as well.

Using regular expressions may help, for example, when you want to subscribe to all order's events, or during debugging. You can use this code if you want to be notified each time an event is fired:

  Spree::Event.subscribe /.*\.spree/ do |event|
    puts "#{event.name} => #{event.payload.inspect}"
  end

Please note that you need to explicitly include the spree namespace in the regexp, or you will subscribe to all ActiveSupport notifications, including the ones from Rails internals.

How Solidus events work

Solidus events are based on Rails' ActiveSupportNotifications, but the actual event backend is configurable, so new event adapters may be added by the core team in the future (or you can create your own, of course).

ActiveSupportNotifications seemed to be the most reasonable choice for implementing the default event adapter for the following reasons:

Solidus events are a relatively new feature, so they still need to become widely used in the Solidus codebase, but developers can start using them anywhere for their convenience by adding custom events and subscribing to them. And if you think you found a good use case, then please consider sharing it with the community by opening an issue or a PR in Solidus... thank you!