Debugging a fluent method chain in Laravel often involves breaking them up. When a long pipeline of filter(), map(), and sortBy() returns an unexpected value, you might want to break the chain and dump out some of the intermediate values with dump() or Log::info().
This leaves your code messy and once you're done debugging, you need to re-build the method chain. Laravel collections have a neat way of doing this - the tap() method.
Table of Contents
The Friction of Breaking Chains
Imagine debugging an order processing pipeline to see how many items remain after filtering:
// The usual debugging habit: breaking the chain
$filtered = $orders->filter(fn ($order) => $order->isPending());
dump($filtered->count()); // Temporary inspection
$total = $filtered->map(fn ($order) => $order->total_amount)->sum();
You introduce a temporary variable ($filtered) that you only use to check the intermediate state. And you have to put the chain back together when you've fixed the bug.
How Collection tap() Works
The collection tap() method passes the current collection instance to a closure and then returns the original collection unchanged. Whatever you return from your callback is ignored, letting you continue the fluent chain without interruption:
$total = $orders
->filter(fn ($order) => $order->isPending())
->tap(function ($pendingOrders) {
// Inspect or log here — the collection passes straight through
})
->map(fn ($order) => $order->total_amount)
->sum();
Debugging with tap() and dump()
Using tap() with dump() lets you inspect values at any point inside a multi-step transformation without changing the return value of your expression:
$activeEmails = $users
->filter->isActive()
->tap(fn ($active) => dump("Active count: " . $active->count()))
->pluck('email')
->tap(fn ($emails) => dump($emails->toArray()))
->unique()
->values();
Laravel also provides a dedicated dump() method directly on collections (e.g., $users->dump()), however, tap() allows you to execute arbitrary expressions, inspect certain counts, and dumping only a subset of attributes.
Triggering Non-Destructive Side Effects
tap() is also useful in production code for non-destructive operations, such as logging audit trails or firing metrics that do not assign intermediate variables:
namespace App\Services;
use App\Models\Invoice;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class InvoiceAggregator
{
public function getOverdueSummaries(): Collection
{
return Invoice::where('is_paid', false)
->get()
->filter->isOverdue()
->tap(function ($overdue) {
Log::warning('Overdue invoices detected', [
'count' => $overdue->count(),
'total' => $overdue->sum('balance'),
]);
})
->values();
}
}
The logging step executes in place, while the method returns the filtered collection directly.
Collection tap() vs Global tap() Helper
Laravel includes both a collection method and a global helper with the same name. Knowing the difference prevents confusion:
| Variant | Syntax | Target Scope |
|---|---|---|
| Collection Method | $collection->tap(fn ($c) => ...) |
Chained directly inside collection pipelines. |
| Global Helper | tap($object, fn ($o) => ...) |
Works on any PHP object, model, or value. |
Summary
When you need to inspect an intermediate value, write to a log, or perform some kind of side effect inside a collection pipeline, you shouldn't use a disposable variable, but rather tap() to peek in and perform your operations, and keep the fluent chain intact.
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬