-
-
Notifications
You must be signed in to change notification settings - Fork 12
Interactors
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
There are two kinds of interactors built into ActiveInteractor: basic interactors and organizers.
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.
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.
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
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
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 ...>>
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