Interaction Reference
Complete reference for business logic Interactions.
Overview
Interactions encapsulate business logic for custom actions. They:
- Accept input from users
- Validate that input
- Execute business logic
- Return success or failure outcomes
Base Class
# app/interactions/resource_interaction.rb (generated during install)
class ResourceInteraction < Plutonium::Resource::Interaction
end
# app/interactions/publish_post_interaction.rb
class PublishPostInteraction < ResourceInteraction
# Interaction code
endPresentation
Configure how the action appears in the UI:
class PublishPost < Plutonium::Resource::Interaction
presents label: "Publish Post",
icon: Phlex::TablerIcons::Send,
description: "Make this post visible to the public"
endAccess presentation metadata:
PublishPost.label # => "Publish Post"
PublishPost.icon # => Phlex::TablerIcons::Send
PublishPost.description # => "Make this post visible..."Attributes
Define inputs using ActiveModel attributes:
Basic Types
attribute :title, :string
attribute :count, :integer
attribute :price, :decimal
attribute :active, :boolean
attribute :published_at, :datetimeWith Defaults
attribute :status, :string, default: "pending"
attribute :notify, :boolean, default: true
attribute :count, :integer, default: 1
attribute :created_at, :datetime, default: -> { Time.current }The resource Attribute
For record actions, declare a resource attribute:
class PublishPost < Plutonium::Resource::Interaction
attribute :resource # The record being acted upon
private
def execute
resource.update!(published: true)
succeed(resource)
end
endThe resources Attribute
For bulk actions, declare a resources attribute:
class BulkArchive < Plutonium::Resource::Interaction
attribute :resources # Collection of records
private
def execute
resources.update_all(archived: true)
succeed(resources)
end
endForm Inputs
Define how attributes render in forms using the input method:
class InviteUser < Plutonium::Resource::Interaction
attribute :resource
attribute :email, :string
attribute :role, :string
input :email, as: :email
input :role, as: :select, choices: %w[admin member viewer]
endSee Fields Reference for all input types and options.
Validation
Use standard ActiveModel validations:
class SchedulePost < Plutonium::Resource::Interaction
attribute :resource
attribute :publish_at, :datetime
validates :publish_at, presence: true
validate :publish_at_in_future
private
def publish_at_in_future
if publish_at.present? && publish_at <= Time.current
errors.add(:publish_at, "must be in the future")
end
end
endValidations run automatically before execute. If invalid, returns a failure outcome.
The execute Method
Main logic goes here. Must return an outcome using succeed() or failed():
private
def execute
resource.update!(published: true)
succeed(resource).with_message("Published!")
rescue ActiveRecord::RecordInvalid => e
failed(e.record.errors)
endHandle RecordInvalid
ActiveRecord::RecordInvalid is not rescued automatically. Always rescue it when using bang methods (create!, update!, save!).
Constructor
Interactions require view_context: and accept attributes as keyword arguments:
interaction = PublishPost.new(
view_context: view_context,
resource: post,
notify: true
)The controller handles this automatically for interactive actions.
Calling Interactions
Via call Class Method
outcome = PublishPost.call(view_context: view_context, resource: post)
if outcome.success?
# Handle success
else
# Handle failure
endVia call Instance Method
interaction = PublishPost.new(view_context: view_context, resource: post)
outcome = interaction.callSuccess Outcomes
Automatic Redirect
On success, the controller automatically redirects to the resource.
Basic Success
succeed(resource) # Redirects to resource automaticallyWith Message
succeed(resource).with_message("Post published!")
succeed(resource).with_message("Warning: limited visibility", :alert)With Custom Redirect
Useful when redirecting somewhere other than the default:
succeed(resource).with_redirect_response(custom_dashboard_path)With File Download
succeed(resource).with_file_response(file_path, filename: "report.pdf")With Render
succeed(resource).with_render_response(:custom_template)Chaining
succeed(resource)
.with_message("Created!")
.with_redirect_response(edit_post_path(resource))Failure Outcomes
Simple Failure
failed("Cannot publish draft posts")With Attribute
failed("is invalid", :email)With Hash of Errors
failed(email: "is invalid", name: "is required")With ActiveModel Errors
failed(resource.errors)Manual Error Addition
def execute
errors.add(:base, "Post must have content")
return failure if errors.any?
# Continue...
endChaining Interactions
Use and_then to chain operations. On failure, the chain short-circuits:
def execute
CreateUserInteraction.call(view_context:, **user_params)
.and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
.and_then { |result| LogActivity.call(view_context:, user: result.value) }
.with_message("User created and welcomed!")
endAccessing Current User
def execute
resource.update!(updated_by: current_user)
succeed(resource)
end
# current_user is provided by the base class:
# def current_user
# view_context.controller.helpers.current_user
# endComplete Example
class Company::InviteUserInteraction < Plutonium::Resource::Interaction
presents label: "Invite User",
icon: Phlex::TablerIcons::UserPlus,
description: "Send an invitation email"
attribute :resource # The company
attribute :email, :string
attribute :role, :string
input :email, as: :email
input :role, as: :select, choices: -> { UserInvite.roles.keys }
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
validate :not_already_invited
private
def execute
invite = UserInvite.create!(
company: resource,
email: email,
role: role,
invited_by: current_user
)
UserInviteMailer.invitation(invite).deliver_later
succeed(resource).with_message("Invitation sent to #{email}")
rescue ActiveRecord::RecordInvalid => e
failed(e.record.errors)
end
def not_already_invited
return unless email.present?
if UserInvite.exists?(company: resource, email: email, state: :pending)
errors.add(:email, "already has a pending invitation")
end
end
endConnecting to Definitions
Register interactions as actions in definitions:
class PostDefinition < Plutonium::Resource::Definition
action :publish, interaction: PublishPostInteraction
action :invite_user, interaction: InviteUserInteraction
action :archive,
interaction: ArchiveInteraction,
confirmation: "Are you sure?",
category: :danger,
position: 100
endImmediate vs Form Actions
Plutonium determines if an action needs a form based on whether inputs are defined:
Shows form first (has inputs):
class InviteUserInteraction < Plutonium::Resource::Interaction
attribute :resource
attribute :email
input :email # This triggers form display
endExecutes immediately (no inputs):
class ArchiveInteraction < Plutonium::Resource::Interaction
attribute :resource
# No inputs = immediate execution with confirmation
endPolicy Integration
Control access with policy methods matching the action name:
class PostPolicy < Plutonium::Resource::Policy
def publish?
update? && record.draft?
end
def archive?
destroy? && !record.archived?
end
endTesting
RSpec.describe PublishPost do
let(:view_context) { double("view_context", controller: double(helpers: double(current_user: user))) }
let(:user) { create(:user) }
let(:post) { create(:post, user: user, published: false) }
describe '#call' do
it 'publishes the post' do
interaction = described_class.new(view_context: view_context, resource: post)
outcome = interaction.call
expect(outcome).to be_success
expect(post.reload).to be_published
end
context 'when validation fails' do
it 'returns failure outcome' do
interaction = described_class.new(view_context: view_context, resource: nil)
outcome = interaction.call
expect(outcome).to be_failure
end
end
end
endBest Practices
- Keep interactions focused - One action per interaction
- Use validations - Validate all inputs before execution
- Handle errors gracefully - Rescue exceptions and return
failed()
Related
- Actions Reference - Connecting interactions to definitions
- Fields Reference - Input configuration
- Policy Reference - Authorization
