I certainly have plenty of ar callbacks, wondering what patterns you use to avoid controller bloat or factories for every model?
Sometimes option 1) Extract the callback to its own method and call it:
# File app/models/user.rb
class User < ApplicationRecord
- after_commit { |user| Mailer.registration_confirmation(user).deliver_later }, on: :create
+ def send_registration_confirmation
+ Mailer.registration_confirmation(self).deliver_later
+ end
...
# File app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.build(user_params)
if @user.save
+ @user.send_registration_confirmation
flash[:notice] = "Registered!"
redirect_to "/"
else
flash.now[:alert] = "Oops!"
render :new
end
end
And sometimes option 2) Introduce a method to do all the work:
Option 2a - Instance method variation:
# File app/models/user.rb
class User < ApplicationRecord
- after_commit { |user| Mailer.registration_confirmation(user).deliver_later }, on: :create
+ def save_and_send_registration_confirmation
+ saved = self.save
+ Mailer.registration_confirmation(self).deliver_later if saved
+ return saved
+ end
...
# File app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.build(user_params)
- if @user.save
+ if @user.save_and_send_registration_confirmation
flash[:notice] = "Registered!"
redirect_to "/"
else
flash.now[:alert] = "Oops!"
render :new
end
end
Option 2b - Class method variation:
# File app/models/user.rb
class User < ApplicationRecord
- after_commit { |user| Mailer.registration_confirmation(user).deliver_later }, on: :create
+ def self.register(attrs)
+ user = build(attrs)
+ Mailer.registration_confirmation(user).deliver_later if user.save
+ user
+ end
...
# File app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
- @user = User.build(user_params)
+ @user = User.register(user_params)
- if @user.save
+ if @user.persisted?
flash[:notice] = "Registered!"
redirect_to "/"
else
flash.now[:alert] = "Oops!"
render :new
end
end
Or 3) Introduce a service object (usually with a class named <Verb><Model>
) to do all the work:
# File app/services/register_user.rb
class RegisterUser
def self.call(params)
user = User.build(params)
if user.save
Mailer.registration_confirmation(user).deliver_later
end
user
end
end
...
# File app/models/user.rb
class User < ApplicationRecord
- after_commit { |user| Mailer.registration_confirmation(user).deliver_later }, on: :create
...
# File app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
- @user = User.build(user_params)
- if @user.save
+ @user = RegisterUser.call(user_params)
+ if @user.persisted?
flash[:notice] = "Registered!"
redirect_to "/"
else
flash.now[:alert] = "Oops!"
render :new
end
end
Option 4) doesn't remove the callback, but is a technique to consider if keeping the callback is necessary. It publishes an event and subscribes to it. This layer of indirection reduces discoverability and clarity but can help alleviate some of the problems with callbacks. This example uses ActiveSupport::Notifications
. Alternatively it could be implemented with wisper.
# File app/models/user.rb
class User < ApplicationRecord
- after_commit { |user| Mailer.registration_confirmation(user).deliver_later }, on: :create
+ after_commit { |user|
+ ActiveSupport::Notifications.instrument("user.created", user)
+ }, on: :create
...
+# File config/initializers/subscriptions.rb
+ActiveSupport::Notifications.subscribe("user.created") do |*, user|
+ Mailer.registration_confirmation(user).deliver_later
+end
...
# File app/controllers/users_controller.rb (no change)
class UsersController < ApplicationController
def create
@user = User.build(user_params)
if @user.save
flash[:notice] = "Registered!"
redirect_to "/"
else
flash.now[:alert] = "Oops!"
render :new
end
end