Refactoring sang Action Classes trong Laravel

· 4 min read

Khi ứng dụng Laravel phát triển, chúng ta thường gặp phải vấn đề "Fat Controller" (Controller phình to) hoặc "God Service" (Service làm quá nhiều việc). Các Controller trở nên rối rắm với validation, logic nghiệp vụ, và định dạng phản hồi. Các Service class thì phình to không kiểm soát, chứa hàng tá methods ít liên quan như createUser, updateUser, notifyUser, exportUser, v.v.

Action Classes (Lớp Hành động) là một mô hình mạnh mẽ để giải quyết vấn đề này. Một Action Class chỉ làm chính xác một việc duy nhất.

Action Class là gì?

Đó là một class PHP đơn giản thực hiện một tác vụ cụ thể. Nó thường chỉ có một public method, thường được đặt tên là execute, handle, hoặc __invoke.

Vấn đề: Controller bị phình to

// UserController.php

public function store(Request $request)
{
    $data = $request->validate([...]);
    
    // Logic nghiệp vụ bị trộn lẫn với logic controller
    $user = User::create([
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);
    
    if ($request->has('newsletter')) {
        Newsletter::subscribe($user->email);
    }
    
    Mail::to($user)->send(new WelcomeEmail());
    
    return redirect()->route('dashboard');
}

Code này rất khó để test độc lập và khó tái sử dụng (ví dụ: nếu bạn muốn tạo user từ một Artisan command).

Giải pháp: CreateUserAction

Hãy tách logic nghiệp vụ ra một class chuyên biệt.

// app/Actions/User/CreateUserAction.php

namespace App\Actions\User;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

class CreateUserAction
{
    public function execute(array $data, bool $subscribeToNewsletter = false): User
    {
        $user = User::create([
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'name' => $data['name'],
        ]);

        if ($subscribeToNewsletter) {
            // Giả sử ta có action hoặc service riêng cho việc này
            Newsletter::subscribe($user->email);
        }

        Mail::to($user)->send(new WelcomeEmail());

        return $user;
    }
}

Refactor lại Controller

Bây giờ Controller chỉ đóng vai trò là giao diện HTTP.

// UserController.php

public function store(Request $request, CreateUserAction $createUser)
{
    $validated = $request->validate([
        'name' => 'required',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
        'newsletter' => 'boolean'
    ]);

    $user = $createUser->execute(
        $validated, 
        $request->boolean('newsletter')
    );

    return redirect()->route('dashboard');
}

Lợi ích của Actions

  1. Tái sử dụng (Reusability): Bạn có thể gọi CreateUserAction từ Controller, Artisan Command, Job, hoặc Seeder.
  2. Khả năng kiểm thử (Testability): Bạn có thể viết unit test cho Action mà không cần lo lắng về HTTP requests, sessions, hay redirects.
  3. Dễ đọc (Readability): Tên class mô tả chính xác việc nó làm. Việc scan code trở nên dễ dàng hơn.
  4. Khả năng kết hợp (Composition): Các Actions có thể gọi lẫn nhau. Ví dụ CreateUserAction có thể gọi SubscribeToNewsletterAction.

Lưu ý khi triển khai

Một số lập trình viên thích biến Action thành "invokable" controller.

// app/Actions/User/CreateUser.php

class CreateUser
{
    public function __invoke(UserData $data) 
    {
        // ...
    }
}

Mặc dù tiện lợi, tôi vẫn ưu tiên việc tách biệt lớp HTTP (Controller) khỏi lớp Domain (Action). Hãy giữ cho Actions của bạn "framework-agnostic" (không phụ thuộc vào request/response của framework) nếu có thể.

Khi nào KHÔNG nên dùng?

Đừng "over-engineer" (kỹ thuật quá mức) những chức năng CRUD đơn giản. Nếu controller của bạn chỉ đơn thuần Post::create($request->all()), bạn chưa cần đến Action class. Hãy áp dụng pattern khi bạn bắt đầu cảm thấy sự phức tạp khó quản lý.

Bình luận