Many times when we try to keep our controllers skinny, we end up moving our business logic to the model. In general, this is good practice but there are cases where models are not a good fit to our needs either.
One such case is when we need to trigger mailers upon certain user actions. For example:
1 class PostsController < ApplicationController
2 def create
3 @post = Post.new permitted_params
4
5 respond_to do |format|
6 if @post.save
7 PostMailer.notify(@post).deliver
8 format.html { redirect_to @post, notice: 'Successfully created Post' }
9 else
10 format.html { render action: 'new' }
11 end
12 end
13 end
14 end
One approach I've personally been guilty of in the past is tying mailer invocations to the model lifecycle:
1 class Post < ActiveRecord::Base
2 after_create do
3 PostMailer.notify(self).deliver
4 end
5 end
Unfortunately this approach has several pitfalls, the main one in my use case being that multiple sources in our application can trigger the model lifecylce callbacks and it becomes a maintenance headache trying to prevent unwanted execution of these actions that really should only be triggered when instigated by a user request.
Fortunately for us though, Rails is natively equipped with a great notification system, namely ActiveSupport::Notifications
.
By taking advantage of this notification system and controller after_action
callbacks, we will create a mailer module
using the observer pattern.
1 module MailerCallbacks
2 module ControllerExtensions
3 def self.included(base)
4 base.after_action do |controller|
5 ActiveSupport::Notifications.instrument(
6 "mailer_callbacks.#{controller_path}##{action_name}", controller: controller
7 )
8 end
9 end
10 end
11
12 module Listener
13 def listen_to(action, &block)
14 ActiveSupport::Notifications.subscribe("mailer_callbacks.#{action}") do |*args|
15 event = ActiveSupport::Notifications::Event.new(*args)
16 controller = event.payload[:controller]
17 controller.instance_eval(&block)
18 end
19 end
20 end
21 end
Now all we need is to create an initializer and register our mailer callbacks:
1 # config/initializers/mailer_callbacks.rb
2 ActiveSupport.on_load(:action_controller) do
3 include MailerCallbacks::ControllerExtensions
4 end
5
6 class MailerListeners
7 extend MailerCallbacks::Listener
8
9 # register as many listeners as you would like here
10
11 listen_to 'posts#create' do
12 PostMailer.notify(@post).deliver if @post.persisted?
13 end
14 end
From a code maintenance point of view, most if not all of our mailer invocations could now be found in a single source file.
This is a general purpose pattern that I've adopted in different contexts that allows me to keep my controllers skinny and isolate independent concerns.