At Super Good we spend a lot of time working on Rails applications, and more specifically applications which include a Solidus store. Often there is a use case for sharing some functionality between Solidus stores. The best way to do that is by developing a Solidus extension, which usually takes the form of a Rails engine. You can think of an engine as a Rails application which can be embedded in another Rails application.
Developing a Rails engine is slightly different than developing a Rails application. For one, to run tests for an engine you have to generate an actual Rails application to host the engine.
There are two gems that simplify writing Solidus extensions, but at first they can be somewhat confusing to navigate. These gems are solidus_support and solidus_dev_support. They each solve a different problem.
The main goal of the solidus_support
gem is to provide extension developers a
compatibility layer for their code so it can work against a wider range of
Solidus and Rails versions. In this post we will take a closer look at some of
the tools it provides.
This feature backports versioned migrations to versions of Rails which don’t support them (versions prior to v5.0). If you are writing an extension and want to maintain support for older versions of Rails, this feature may be useful so you don’t have to maintain separate versions of your source. You can use the provided base class the same way as you would the one provided by Rails:
# On Rails 4.2 returns `ActiveRecord::Migration`
class AddBrickAndMortarStores < SolidusSupport::Migration[4.2]
def self.up
create_table :brick_and_mortar_stores, force: true do |table|
table.string :name, null: false
table.json :hours, null: false
table.integer :address_id, null: false
table.timestamps
end
end
def self.down
drop_table :brick_and_mortar_stores
end
end
You only need to use this feature if your extension needs to support versions of Rails older than v5.0.
Most of the functionality provided by this gem is exposed through the
EngineExtensions
module. You can include it in your extension’s engine class
like this:
module SuperGoodStoreLocator
class Engine < Rails::Engine
engine_name 'super_good_store_locator'
include SolidusSupport::EngineExtensions
end
end
This enables many quality-of-life features in your extension. But I want to touch on two specifically: automatic override code loading, and a compatibility layer for Solidus’s new Omnes event bus.
In order to make customizations to the parts of Solidus that aren’t configurable, extensions often use overrides. (Older versions of the Solidus documentation called these “decorators”.)
Extensions which rely on decorators get auto-loading when the files are placed
in the lib/decorators/*
folder. In addition to that, solidus_support
provides
conditional loading of files based on which Solidus engines are present in a host
application. When stores opt to use only individual Solidus components, this
utility allows your extension to provide behaviour which extends solidus_core
/ solidus_api
/ solidus_backend
without having to test whether each one is
loaded, simply by placing the files in a folder named after the extension.
For example, if your extension provides functionality specific to
solidus_backend
, adding your code to the following folders will result in it
being loaded only if the host application includes that engine:
lib/views/backend
lib/controllers/backend
lib/decorators/backend
For example, if you’re adding an admin interface for your store locator extension, you might want to add a controller and some views:
lib/controllers/backend/brick_and_mortar_stores_controller.rb
lib/views/backend/brick_and_mortar_stores/_form.html.erb
lib/views/backend/brick_and_mortar_stores/edit.html.erb
lib/views/backend/brick_and_mortar_stores/index.html.erb
lib/views/backend/brick_and_mortar_stores/new.html.erb
lib/views/backend/brick_and_mortar_stores/show.html.erb
And if the engine is included in the Rails application, these files would be
loaded. If, for some reason, a store using the extension isn’t using
solidus_backend
, the controller and views won’t be loaded. This way, we won’t run
into dependency failures if the Spree::Admin::BaseController
provided by
solidus_backend
isn’t available.
By default solidus_support
loads event subscriber files from the
lib/subscribers/*
folder in extensions. Something even more useful is
the forwards compatibility layer for the new event system which was introduced
in Solidus 3.2. The previous implementation relied on
ActiveSupport::Notifications
. The new event system is built on
Omnes.
This change requires you to upgrade event subscribers
to the new version of the event bus.
You can take advantage of the SolidusSupport::LegacyEventCompat::Subscriber
module to make your existing event subscribers work with Omnes without having to
rewite them. This means you don’t have to maintain separate versions in order
to support both event systems.
module SuperGoodStoreLocator
module BrickAndMortarStoreSubscriber
include ::Spree::Event::Subscriber
include SolidusSupport::LegacyEventCompat::Subscriber
event_action :store_hours_changed
def store_hours_changed(event)
UpdateGoogleMapsInfoJob
.perform_later(event.payload[:brick_and_mortar_store].hours)
end
end
end
If your extension adds custom events and you want that to continue to work in applications running on Solidus 3.2 you can use the provided compatibility layer to do that
SolidusSupport::LegacyEventCompat::Bus
.publish(:store_hours_changed, brick_and_mortar_store: brick_and_mortar_store)
On applications which still use the legacy event bus, this is equivalent to
Spree::Event
.fire(:store_hours_changed, brick_and_mortar_store: brick_and_mortar_store)
and for applications that have opted-in to the new default, this uses the new API
Spree::Bus
.publish(:store_hours_changed, brick_and_mortar_store: brick_and_mortar_store)
Hopefully all this has convinced you of the many benefits of building your
Solidus extension on top of the functionality solidus_support
provides. The
Solidus community has done a great job of ensuring that developers have the right
tools to write extensions in a more maintainable and backwards compatible way.