Skip to content

Instantly share code, notes, and snippets.

@eliotsykes
Last active August 30, 2022 05:46
Show Gist options
  • Save eliotsykes/41fcee0a5ccedf50aaee3cd3f6bdf03a to your computer and use it in GitHub Desktop.
Save eliotsykes/41fcee0a5ccedf50aaee3cd3f6bdf03a to your computer and use it in GitHub Desktop.
Techniques for Removing ActiveRecord Callbacks

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment