tap

Pass a value to a closure for side effects and return the original value.

Signature
tap(mixed $value, callable|null $callback = null): mixed

Parameters

ParameterTypeDescription
$valuemixedThe value to pass to the callback and hand back to the caller.
$callbackoptionalcallable|null= nullInvoked 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 a HigherOrderTapProxy. 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 integer update() 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.