tap
Pass a value to a closure for side effects and return the original value.
tap(mixed $value, callable|null $callback = null): mixedParameters
| Parameter | Type | Description |
|---|---|---|
$value | mixed | The value to pass to the callback and hand back to the caller. |
$callbackoptional | callable|null= null | Invoked with the value. Its return value is discarded. When omitted, tap returns a HigherOrderTapProxy instead. |
Return value
mixedThe original $value when a callback is given; a HigherOrderTapProxy wrapping the value when the callback is omitted.
tap() hands a value to a closure so you can do something with it, then returns that same value. The closure's return value is thrown away. It exists so a side effect does not force you to break a chain into a temporary variable.
The two forms
- With a closure —
tap($value, fn ($v) => ...)returns$value. - Without a closure —
tap($value)returns aHigherOrderTapProxy. Every method you call on the proxy is forwarded to the value, the method's own return value is discarded, and the value comes back instead. This is how$user = tap($user)->update([...])gives you the model rather than the integerupdate()normally returns.
When to reach for it
Reach for it when a method you need to call returns the wrong thing — a bool, an int, void — and you want the subject back so the expression can continue. It is also the tidiest way to create, mutate and return an object in a single statement, and a convenient place to hang a temporary dump() mid-chain.
Mutation still matters
Objects are handed to the closure by handle, so changes inside are visible outside. Scalars and arrays are not — mutating a copy of a string inside the closure changes nothing.
Example
When to use it
- Update a model and get the model back instead of the affected-row count.
- Build, configure and return an object in one statement without a temporary variable.
- Drop a dump() into the middle of a chain without restructuring the code.
Frequently asked questions
What is the difference between tap() and with()?
tap() returns the original value and discards the callback's result; with() returns the callback's result. Use tap for side effects on a subject you want back, and with when the closure computes the thing you actually want.
Why does tap() ignore what my closure returns?
By design — the helper exists to keep the subject flowing through an expression, so the closure is treated purely as a side effect. If you need the transformed value, use with(), pipe() on a Collection, or just assign the result directly.
How does tap() work without a closure?
It returns a HigherOrderTapProxy that forwards any method call to the underlying value, throws away that method's return value, and returns the value itself. That is what makes tap($user)->update([...]) evaluate to the user.
More examples
The higher-order proxy on a plain object
bump() genuinely returns an integer, yet $counter is the Counter object with its side effect applied. That is the whole point of the no-closure form: it inverts what a method hands back.
<?php
class Counter
{
public int $n = 0;
public function bump(): int
{
return ++$this->n;
}
}
// bump() returns an int, but the proxy discards it.
$counter = tap(new Counter)->bump();
var_dump($counter instanceof Counter); // bool(true)
var_dump($counter->n); // int(1)
// Without tap you would need two statements.
$other = new Counter;
$other->bump();Update and return a model
The first method is the canonical use: Eloquent's update() returns the number of affected rows, which is useless as a controller response, so tap swaps it for the model. The second shows the closure form doing two side effects and still returning the new record. Needs a booted application.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function update(Request $request, User $user)
{
// update() returns an int; tap gives back the User.
return tap($user)->update($request->only('name', 'email'));
}
public function store(Request $request)
{
return tap(new User($request->validated()), function (User $user) {
$user->save();
$user->sendWelcomeNotification();
});
}
}Inspecting a chain without breaking it
tap inside pipe() prints the intermediate collection and passes it straight through, so the sum still runs on the same data. Swap print_r for dump() in an application and you have a one-line probe you can delete later.
<?php
$total = collect([1, 2, 3, 4, 5])
->filter(fn (int $n) => $n % 2 === 1)
->pipe(fn ($odds) => tap($odds, fn ($c) => print_r($c->values()->all())))
->sum();
echo $total, "\n";
// Array ( [0] => 1 [1] => 3 [2] => 5 )
// 9
Discussion