Pipeline Pattern trong Laravel: Code sạch cho Logic phức tạp

· 5 min read

Đôi khi controllers hoặc services trở nên "fat" với logic thủ tục (procedural) thực thi các bước theo một thứ tự cụ thể.

Ví dụ, đặt hàng (placing an order) bao gồm:

  1. Validate kho hàng
  2. Tính toán giảm giá
  3. Thanh toán
  4. Gửi thông báo
  5. Làm trống giỏ hàng

Đặt tất cả điều này trong một method dẫn đến spaghetti code. Pipeline Pattern cho phép bạn chia quy trình này thành các class riêng lẻ (Pipes) mà dữ liệu đi qua đó.

Laravel Pipeline Utility

Laravel sử dụng pipelines nội bộ cho Middleware. Bạn có thể sử dụng cùng utility đó cho logic của riêng bạn.

use Illuminate\Support\Facades\Pipeline;

public function store(Request $request)
{
    $order = Pipeline::send($request->all())
        ->through([
            ValidateInventory::class,
            ApplyDiscounts::class,
            ProcessPayment::class,
            SendOrderEmail::class,
        ])
        ->then(fn ($content) => Order::create($content));

    return response()->json($order);
}

Tạo một Pipe

Mỗi pipe class cần một method handle. Nó nhận content (passable) và một Closure $next.

1. Validate Inventory

class ValidateInventory
{
    public function handle($content, Closure $next)
    {
        if (! Inventory::hasStock($content['product_id'])) {
            throw new OutOfStockException();
        }

        return $next($content);
    }
}

2. Tính toán giảm giá

class ApplyDiscounts
{
    public function handle($content, Closure $next)
    {
        if (isset($content['coupon_code'])) {
            $content['price'] = $content['price'] * 0.9; // Giảm 10%
        }

        return $next($content);
    }
}

3. Xử lý thanh toán

class ProcessPayment
{
    public function handle($content, Closure $next)
    {
        // Gọi Stripe/PayPal
        PaymentGateway::charge($content['price']);

        return $next($content);
    }
}

Dependency Injection

Pipes được resolve qua Service Container, vì vậy bạn có thể inject dependencies trong constructor.

class SendOrderEmail
{
    public function __construct(
        protected Mailer $mailer
    ) {}

    public function handle($content, Closure $next)
    {
        $this->mailer->to($content['email'])->send(new OrderConfirmed());
        
        return $next($content);
    }
}

Sử dụng tên Method tùy chỉnh

Nếu bạn không muốn sử dụng handle, bạn có thể chỉ định tên method qua via().

Pipeline::send($order)
    ->via('process')
    ->through($pipes)
    ->then(function ($order) {
        // ...
    });

Advanced Data Objects

Truyền một array $content có thể rủi ro (không có type safety). Thay vào đó, truyền một Object hoặc DTO.

class OrderContext
{
    public User $user;
    public Collection $items;
    public float $total = 0;
    public ?string $error = null;
}

// Usage
$context = new OrderContext($user, $items);

Pipeline::send($context)
    ->through([...])
    ->then(function ($context) {
        // Tất cả pipes đã sửa đổi object $context này
        $context->save();
    });

Ngắt Pipeline

Nếu một pipe không gọi $next($content), pipeline sẽ dừng lại. Điều này hữu ích cho các validation failures hoặc logic có điều kiện.

class CheckFraud
{
    public function handle($content, Closure $next)
    {
        if ($this->isFraudulent($content)) {
            // Dừng thực thi, trả về lỗi
            return response()->json(['error' => 'Fraud detected'], 400); 
        }

        return $next($content);
    }
}

Lưu ý: Nếu bạn trả về response sớm, callback then() cuối cùng sẽ KHÔNG thực thi (trừ khi bạn cấu trúc nó để xử lý giá trị trả về đó).

Reusable Pipelines

Bạn có thể đóng gói cấu hình pipeline trong một class chuyên dụng.

class OrderProcessingPipeline
{
    protected $pipes = [
        ValidateInventory::class,
        ApplyDiscounts::class,
        ProcessPayment::class,
    ];

    public function run(Order $order)
    {
        return Pipeline::send($order)
            ->through($this->pipes)
            ->thenReturn(); // Trả về object được truyền vào
    }
}

Khi nào nên dùng?

✅ Dùng khi:

  • Bạn có một quy trình tuần tự với hơn 3 bước.
  • Các bước độc lập hoặc ít phụ thuộc nhau (loosely coupled).
  • Bạn cần thực thi các bước theo điều kiện.
  • Bạn muốn unit test từng bước một cách cô lập.

❌ Tránh khi:

  • Logic đơn giản (1-2 bước).
  • Các bước phụ thuộc chặt chẽ (bước 3 dựa nhiều vào nội bộ của bước 1).
  • Bạn cần transactional database rollback qua tất cả các bước (mặc dù bạn có thể bọc pipeline trong DB::transaction).

Tóm tắt

Pipeline pattern cho phép bạn viết:

  • Testable code: Mỗi pipe là một đơn vị nhỏ, có thể kiểm thử.
  • Readable code: Method controller chính đọc giống như một mục lục.
  • Flexible code: Sắp xếp lại, thêm hoặc xóa các bước mà không chạm vào logic cốt lõi.

Đây là một trong những tính năng ít được tận dụng nhất của Laravel để refactoring legacy codebases.

Bình luận