Clean Code với Data Transfer Objects (DTO) trong Laravel

· 4 min read

Trong nhiều ứng dụng Laravel—đặc biệt khi chúng phát triển—chúng ta thường thấy "array spaghetti." Controller truyền associative array đến Service, Service truyền chúng đến private method, và cuối cùng, không ai biết chính xác những key nào có sẵn hoặc kiểu dữ liệu của chúng.

Đây là lúc Data Transfer Object (DTO) xuất hiện.

Vấn Đề với Array

Hãy xem xét tương tác Controller-to-Service điển hình này:

// UserController.php
public function store(Request $request, UserService $service)
{
    // Chính xác có gì trong array này? 
    // 'email_verified_at' có được bao gồm không? 
    // 'is_admin' có phải boolean hay 0/1?
    return $service->createUser($request->validated());
}

// UserService.php
public function createUser(array $data)
{
    // Chúng ta phải đoán hoặc kiểm tra key thủ công
    $email = $data['email'] ?? null; 
}

Cách tiếp cận này thiếu type safety, không cung cấp IDE autocompletion, và khiến refactoring trở thành ác mộng.

Giải Pháp: DTO PHP 8.2+ Native

Với sự ra đời của readonly class trong PHP 8.2, việc tạo DTO immutable chưa bao giờ sạch hơn. Chúng ta không nhất thiết cần package nặng; PHP native hoạt động rất đẹp.

1. Định Nghĩa DTO

Tạo thư mục app/DTOs và định nghĩa class:

namespace App\DTOs;

readonly class UserData
{
    public function __construct(
        public string $name,
        public string $email,
        public ?string $password = null,
        public bool $isAdmin = false,
    ) {}

    public static function fromRequest(Request $request): self
    {
        return new self(
            name: $request->validated('name'),
            email: $request->validated('email'),
            password: $request->validated('password'),
            isAdmin: $request->boolean('is_admin', false),
        );
    }
}

2. Cập Nhật Service

Bây giờ service của bạn biết chính xác nó đang nhận gì:

namespace App\Services;

use App\DTOs\UserData;
use App\Models\User;

class UserService
{
    public function createUser(UserData $data): User
    {
        // IDE biết chính xác những property nào tồn tại!
        return User::create([
            'name' => $data->name,
            'email' => $data->email,
            'password' => bcrypt($data->password),
            'is_admin' => $data->isAdmin,
        ]);
    }
}

3. Cập Nhật Controller

Controller trở thành cầu nối chuyển đổi HTTP input thành domain object:

public function store(StoreUserRequest $request, UserService $service)
{
    $userData = UserData::fromRequest($request);
    
    $user = $service->createUser($userData);

    return new UserResource($user);
}

Nâng Cao: Sử Dụng spatie/laravel-data

Nếu bạn muốn validation, transformation, và TypeScript generation sẵn có, tôi rất khuyến nghị package Giới thiệu laravel-data.

Nó cho phép bạn inject DTO trực tiếp vào controller:

public function store(UserData $userData, UserService $service)
{
    // $userData đã được validate và khởi tạo!
    $service->createUser($userData);
}

Tóm Tắt Lợi Ích

  1. Type Safety: Bắt bug tại compile time (hoặc static analysis time) thay vì runtime.
  2. Immutability: Một khi được tạo, data không thể bị thay đổi bất ngờ bởi side effect trong service method.
  3. Context: Data đã validate từ Request chung được chuyển đổi thành context có ý nghĩa (UserData, PostDraft, SearchFilters).
  4. Hỗ trợ IDE: Autocompletion đầy đủ giúp phát triển nhanh hơn.

Bắt đầu đơn giản với PHP class native, và nâng cấp lên package chỉ khi bạn cần logic transformation phức tạp.

Bình luận