Skip to content

Interactors

Aaron Allen edited this page Oct 12, 2020 · 4 revisions

Most of the time, your application will use its interactors from its controllers. The following controller:

class SessionsController < ApplicationController
  def create
    if user = User.authenticate(session_params[:email], session_params[:password])
      session[:user_token] = user.secret_token
      redirect_to user
    else
      flash.now[:message] = "Please try again."
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

can be refactored to:

class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.perform(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.errors.full_messages)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

given the basic interactor and context:

class AuthenticateUserContext < ActiveInteractor::Context::Base
  attributes :email, :password, :user, :token
  validates :email, presence: true,
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true
  validates :user, presence: true, on: :called
end

class AuthenticateUser < ActiveInteractor::Base
  def perform
    context.user = User.authenticate(
      context.email,
      context.password
    )
    context.token = context.user.secret_token
  end
end

The .perform class method is the proper way to invoke an interactor. The hash argument is converted to the interactor instance's context. The #perform instance method is invoked along with any callbacks and validations that the interactor might define. Finally, the context (along with any changes made to it) is returned

Kinds of Interactors

There are two kinds of interactors built into ActiveInteractor: basic interactors and organizers.

Interactors

A basic interactor is a class that inherits from ActiveInteractor::Base and defines #perform.

class AuthenticateUser < ActiveInteractor::Base
  def perform
    user = User.authenticate(context.email, context.password)
    if user
      context.user = user
      context.token = user.secret_token
    else
      context.fail!
    end
  end
end

Basic interactors are the building blocks. They are your application's single-purpose units of work.

Organizers

An organizer is an important variation on the basic interactor. Its single purpose is to run other interactors.

class CreateOrder < ActiveInteractor::Base
  def perform
    ...
  end
end

class ChargeCard < ActiveInteractor::Base
  def perform
    ...
  end
end

class SendThankYou < ActiveInteractor::Base
  def perform
    ...
  end
end

class PlaceOrder < ActiveInteractor::Organizer::Base

  organize :create_order, :charge_card, :send_thank_you
end

In a controller, you can run the PlaceOrder organizer just like you would any other interactor:

class OrdersController < ApplicationController
  def create
    result = PlaceOrder.perform(order_params: order_params)

    if result.success?
      redirect_to result.order
    else
      @order = result.order
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit!
  end
end

The organizer passes its context to the interactors that it organizes, one at a time and in order. Each interactor may change that context before it's passed along to the next interactor.

Organizing Interactors Conditionally

We can also add conditional statements to our organizer by passing a block to the .organize method with the :if or :unless keys:

class PlaceOrder < ActiveInteractor::Organizer::Base
  organize do
    add :create_order, if :user_registered?
    add :charge_card, if: -> { context.order }
    add :send_thank_you, if: -> { context.order }
  end

  private

  def user_registered?
    context.user&.registered?
  end
end

Organized Interactor Callbacks

We can also add inline callback actions to our organizer by passing a block to the .organize method with the :before or :after keys:

class PlaceOrder < ActiveInteractor::Organizer::Base
  organize do
    add :create_order, before: -> { puts context.order }, after: :print_order_id
    add :charge_card
    add :send_thank_you
  end

  private

  def print_order_id
    puts context.order.id
  end
end

Running Interactors In Parallel

Organizers can be told to run their interactors in parallel with the .perform_in_parallel method. This will run each interactor in parallel with one and other only passing the original context to each interactor. This means each interactor must be able to #perform without dependencies on prior interactor invokations.

class CreateNewUser < ActiveInteractor::Base
  def perform
    context.user = User.create(
      first_name: context.first_name,
      last_name: context.last_name
    )
  end
end

class LogNewUserCreation < ActiveInteractor::Base
  def perform
    context.log = Log.create(
      event: 'new user created',
      first_name: context.first_name,
      last_name: context.last_name
    )
  end
end

class CreateUser < ActiveInteractor::Organizer::Base
  perform_in_parallel
  organize :create_new_user, :log_new_user_creation
end

CreateUser.perform(first_name: 'Aaron', last_name: 'Allen')
#=> <#CreateUser::Context first_name='Aaron' last_name='Allen' user=>#<User ...> log=<#Log ...>>

Rollback

If any one of the organized interactors fails its context, the organizer stops. If the ChargeCard interactor fails, SendThankYou is never called.

In addition, any interactors that had already run are given the chance to undo themselves, in reverse order. Simply define the #rollback method on your interactors:

class CreateOrder < ActiveInteractor::Base
  def perform
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end