
What is the Actions Pattern?
To understand the Actions Pattern, we need to understand the problem it solves.
The problem is that a single unit of business logic is often spread across multiple files. It’s usually coupled tightly with the layers of the application, and is generally harder to test.
For example, let’s say we want to create a new user. We might have a UserController
that looks like this:
class UserController extends Controller
{
public function store(Request $request)
{
$user = User::create($request->all());
return response()->json($user, 201);
}
}
Sure, it’s testable, but coupled with the HTTP layer. What if we want to create a user with a command, or a dashboard tool (e.g. Filament)?
// Controller
class UserController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
'department_id' => 'required|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
]);
try {
DB::beginTransaction();
// Create user with hashed password
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'department_id' => $validated['department_id'],
'manager_id' => $validated['manager_id'],
]);
// Assign default role
$role = Role::where('name', 'employee')->firstOrFail();
$user->roles()->attach($role->id);
// Generate and store API token
$token = $user->createToken('api-token')->plainTextToken;
// Dispatch welcome event
event(new UserCreated($user));
// Send welcome email
Mail::to($user)->send(new WelcomeEmail($user));
DB::commit();
// Return response with token
return response()->json([
'user' => $user->load('roles', 'department'),
'token' => $token
], 201);
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to create user: ' . $e->getMessage());
return response()->json(['error' => 'Failed to create user'], 500);
}
}
}
// Command
class CreateUserCommand extends Command
{
protected $signature = 'user:create {name} {email} {password} {department_id} {--manager_id=}';
public function handle()
{
// Validation (manual for CLI)
$name = $this->argument('name');
$email = $this->argument('email');
$password = $this->argument('password');
$departmentId = $this->argument('department_id');
$managerId = $this->option('manager_id');
// Check if email exists
if (User::where('email', $email)->exists()) {
$this->error('Email already exists!');
return 1;
}
try {
DB::beginTransaction();
// Create user with hashed password
$user = User::create([
'name' => $name,
'email' => $email,
'password' => Hash::make($password),
'department_id' => $departmentId,
'manager_id' => $managerId,
]);
// Assign default role
$role = Role::where('name', 'employee')->firstOrFail();
$user->roles()->attach($role->id);
// Generate and store API token
$token = $user->createToken('api-token')->plainTextToken;
// Dispatch welcome event
event(new UserCreated($user));
// Send welcome email
Mail::to($user)->send(new WelcomeEmail($user));
DB::commit();
$this->info('User created successfully!');
$this->line('API Token: ' . $token);
return 0;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to create user: ' . $e->getMessage());
$this->error('Failed to create user: ' . $e->getMessage());
return 1;
}
}
}
// Filament Action
class CreateUserAction extends Action
{
protected function setUp(): void
{
$this->form([
TextInput::make('name')->required()->maxLength(255),
TextInput::make('email')->email()->required()->unique('users', 'email'),
TextInput::make('password')->password()->required()->minLength(8),
TextInput::make('password_confirmation')->password()->required()->same('password'),
Select::make('department_id')->relationship('departments', 'name')->required(),
Select::make('manager_id')->relationship('users', 'name')->label('Manager'),
]);
}
public function handle(array $data)
{
try {
DB::beginTransaction();
// Create user with hashed password
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'department_id' => $data['department_id'],
'manager_id' => $data['manager_id'] ?? null,
]);
// Assign default role
$role = Role::where('name', 'employee')->firstOrFail();
$user->roles()->attach($role->id);
// Generate and store API token
$token = $user->createToken('api-token')->plainTextToken;
// Dispatch welcome event
event(new UserCreated($user));
// Send welcome email
Mail::to($user)->send(new WelcomeEmail($user));
DB::commit();
Notification::make()
->title('User created successfully')
->success()
->send();
return $user;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Failed to create user: ' . $e->getMessage());
Notification::make()
->title('Failed to create user')
->body($e->getMessage())
->danger()
->send();
return null;
}
}
}
A World Without Actions
// discuss putting logic directly in controllers, or fat models, and the downsides of making those testable, or using that logic somewhere else
Services
// an incrementally better approach but can be even better
Actions
// The ultimate portable business logic molecule
Test Coverage
// Its
