Allow users to extend your library code with events
Enable users to build up on your Rails Engines and Gems without monkey patching, inheritence or overriding methods
This short tutorial is intended for authors of Gems and Engines.
When writing gems or Rails engines they can be difficult for the user to extend.
For example Devise, the authentication Rails engine, has a Wiki page showing how to send an email after a user changes their password.
The Wiki suggests monkey patching your model overriding the update_with_password
method.
def update_with_password(params, *options)
if super
UserMailer.password_changed(self.id).deliver
end
end
Monkey patching is dangerous particually if the method is not part of the public API.
Even patch version updates of Devise could potentially break your code.
In addition the method is not very readable, it seems super
returns a boolean, but what is it actually doing? Can we be sure it will always return a boolean, or have the same method signature…
An alternative would be for Devise to broadcast events which the host application can listen for and react to. This can be achived with the Wisper gem.
In the Devise implementation of update_with_password
they could broadcast an event:
module DatabaseAuthenticatable
extend ActiveSupport::Concern
included do
include Wisper.publisher # <-- LINE ADDED HERE
end
def update_with_password(params, *options)
current_password = params.delete(:current_password)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
result = if valid_password?(current_password)
update_attributes(params, *options)
broadcast(:password_updated, self) # <-- LINE ADDED HERE
else
self.assign_attributes(params, *options)
self.valid?
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
clean_up_passwords
result
end
end
Within our Rails application we could then subscribe a listener for this event in an initializer:
User.subscribe(UserListener.new, prefix: 'on')
In the case of sending an email after the email address has changed the listener would look like this:
class UserListener
def on_password_changed(user_id)
UserMailer.password_changed(user_id).deliver
end
end
The nice thing about this pattern is the Gem/Engine author can broadcast many events, with a single line of code for each and does not need to consider the method name and signature since it no longer needs to be overridden in the Rails app.
On the flip side the Rails application does not need to know about the Gem internals and can be confident changes to the API will not break it.
- Easy for Engine/Gem author to impliment, simply add Wisper as a runtime dependency and use the broadcast method
- Easy for the Engine/Gem user, simply subscribe a listener, no need to monkey patch internals.
On top of this the additional options built in to Wisper are avalible including support for async events, note that the Gem/Engine author does not need to make any changes or add any additional dependencies to allow the Rails app to respond to its events async.
Other places Devise could broadcast events would be signin / signout / change password etc. A listener could subscribe to these events to, for example, record an audit of all logins.
In the above example I subscribed the listener to all instances of User
, but you could also subscribe to indavidual instances:
@user.subscribe(SpecialListener.new, prefix: 'on')
Any gem or engine can use this technique to provide optional, decoupled callbacks. I hope you consider it for your own Gem or Engine.
Happy hacking.