Laravel Authorization Patterns

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

Enjoying the blog? Subscribe to my newsletter!

No spam, completely free.

Subscribe
BlueSky Logo Share on Bluesky
Chris Arter
Chris Arter Software Engineer