TL;DR
pipe() trên Illuminate\Support\Collection nhận đúng một callable, đẩy collection vào đó và trả về kết quả callback. Khác tap() ở chỗ pipe() thay đổi dữ liệu chain theo giá trị return, còn tap() luôn trả về collection gốc. Dùng khi cần nhét logic riêng vào giữa chain mà không muốn tạo macro global. Có 3 anh em họ: pipe(), pipeThrough(), pipeInto().
pipe() là gì
Signature: pipe(callable $callback). Ví dụ chuẩn từ docs Laravel 12.x:
$collection = collect([1, 2, 3]);
$piped = $collection->pipe(function (Collection $collection) {
return $collection->sum();
});
// 6Đơn giản vậy thôi: collection đi vào, kết quả callback đi ra. Không có $next closure, không có middleware, không có magic. Chỉ là một cái cầu để gắn function của bạn vào fluent chain mà không phải dừng chain lại, gán vào biến tạm, rồi gọi function thủ công.
Vì sao cần đến pipe() khi đã có map/filter/reduce
Câu hỏi rất hay — và đó cũng là câu hỏi post trên Laracasts từ 2017. Câu trả lời gọn lại: khi bạn cần gọi một function không thuộc Collection, mà vẫn muốn giữ luồng đọc trái-phải.
Không có pipe():
$users = User::all();
$active = $users->filter(fn ($u) => $u->is_active);
$report = $this->reportFormatter->format($active);
return $report;Có pipe():
return User::all()
->filter(fn ($u) => $u->is_active)
->pipe(fn ($users) => $this->reportFormatter->format($users));Logic không thay đổi. Nhưng dòng đọc liên tục, không bị cắt bằng biến trung gian, và reportFormatter không cần biết tới Collection macro — nó vẫn là service object thuần túy, dễ test, dễ inject.
Anh em họ: pipe(), pipeThrough(), pipeInto()
Collection class thực ra cung cấp ba biến thể, mỗi cái cho một dạng tiêu thụ khác nhau:
| Method | Nhận | Trả về | Khi dùng |
|---|---|---|---|
pipe(callable) | 1 closure | Kết quả closure | Một bước transform tùy biến |
pipeThrough(array) | Array các callable | Kết quả pipe cuối, mỗi pipe nhận return của pipe trước | Nhiều bước tuần tự |
pipeInto(class) | Tên class | Instance mới của class, collection được pass vào constructor | Wrap collection thành DTO/value object |
pipeThrough() được merge vào Laravel v8.78.1 (12/01/2022) qua PR #40253 của Steve Bauman. Bản chất là array_reduce chạy qua mảng callback:
$result = collect([1, 2, 3])->pipeThrough([
fn ($c) => $c->merge([4, 5]),
fn ($c) => $c->sum(),
]);
// 15pipe() vs Pipeline class vs PHP 8.5 |>
Đừng nhầm Collection::pipe() với Illuminate\Pipeline\Pipeline — class mà Laravel dùng nội bộ để chạy middleware HTTP. Hai cái cùng tên "pipe" nhưng kiến trúc khác hẳn:
| Khía cạnh | Collection::pipe() | Illuminate\Pipeline\Pipeline |
|---|---|---|
| Scope | Chỉ Collection | Bất kỳ input nào |
| Số stage | 1 callable | N invokable/closure |
| Cơ chế chuyển bước | Return trực tiếp | Closure $next($payload) |
| Kích hoạt | Tức thời | Phải gọi ->then() / ->thenReturn() |
| Use case nội bộ Laravel | — | Stack middleware HTTP |
Còn PHP 8.5 (cuối 2025) đã ship pipe operator |> ở mức ngôn ngữ — đẩy giá trị bên trái vào function bên phải:
$sum = [1, 2, 3] |> array_sum(...);Liệu |> có thay thế pipe()? Không hẳn. Native operator hợp với chuỗi function thuần (functional style, output type đổi sau mỗi bước); fluent API như Collection hợp với chain method giữ nguyên type. Benchmark thực tế trong Laravel 12 + PHP 8.5 cho thấy |> đắt hơn cách truyền thống 20–45% (đơn vị microsecond, không đáng kể), nhưng đuối khi gặp mutation hoặc function không khớp pattern (vd usort). Tóm: |> bổ sung, không thay thế.
Use case thực tế
- Gắn invokable class vào chain.
->pipe(new ExportToCsv($path))— class chỉ cần__invoke(Collection $c), không cần biết Laravel. - Đổi type ở cuối chain. Trả về JsonResponse, DTO, string CSV — fluent kết thúc đúng nơi bạn muốn.
- One-off logic không đáng tạo macro. Macro sống global trong service provider;
pipe()giữ logic ngay tại điểm dùng. - Wrap thành value object.
->pipeInto(InvoiceTotals::class)đẹp hơnnew InvoiceTotals($collection). - Chuỗi transform nhiều bước. Lúc này dùng
pipeThrough([...])sẽ rõ hơn lồng nhiềupipe().
Limitations & gotchas
- Quên
returntrong closure → mất dữ liệu. Lỗi kinh điển khi nhầm vớitap().tap()luôn trả collection gốc dù closure return gì;pipe()trả đúng cái closure return — nếu không return gì thì bạn nhận vềnull. - Return non-collection sẽ kết thúc fluent chain. Sau
->pipe(fn ($c) => $c->sum())không gọi->map()được nữa. Thường đó là chủ ý — chỉ cần biết để không ngạc nhiên. - Không có introspection giữa các stage. Cần thì dùng
pipeThrough()hoặc classPipeline. - Method có từ rất lâu (đã có trên Laracasts từ 1/2017). Mọi version Laravel còn được support đều có.
Tiếp theo
Nếu code base bạn đang trên PHP 8.5, thử kết hợp: vẫn dùng Collection cho mapping/filtering/grouping, nhưng cuối chain dùng |> để truyền sang chuỗi function thuần (validate, sanitize, format). Còn nếu chưa, pipe() + pipeThrough() đã đủ giải quyết 95% nhu cầu "nhét logic riêng vào fluent chain" mà không phải đẻ thêm macro hay class wrapper.
Nguồn: Laravel docs, Laravel News, Honeybadger, freek.dev.