A Guide to Solidus Events: Everyone's Invited!
Andrea Longhi
23 Mar 2020 - 6 mins read
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:
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:
- it's default Rails
- it's battle tested since Rails 3.x
- it doesn't require any external events library
- it's widely used in Rails internals
- it's simple and easy to use
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!