tap() is a helper for “using a value and still returning that same value.” Use it when you want to insert side effects.The implementation in Illuminate\Support\helpers.php is simple:
function tap($value, $callback = null){ if (is_null($callback)) { return new HigherOrderTapProxy($value); } $callback($value); return $value;}
tap() ignores the callback’s return value. It always returns the original value.
The most basic pattern is: receive a value, run some logic, then return the original value.
use App\Models\User;$user = tap(User::query()->latest()->firstOrFail(), function (User $model) { $model->update(['last_seen_at' => now()]);});// Returns $user regardless of update()'s return value
If you omit the callback, tap() returns a HigherOrderTapProxy, so you can chain method calls directly.
$updatedUser = tap($user)->update([ 'name' => $name, 'email' => $email,]);// Returns the original User instance, not update()'s return value
If you use Illuminate\Support\Traits\Tappable, your class gets a tap() method.
trait Tappable{ public function tap($callback = null) { return tap($this, $callback); }}
In other words, it only provides an instance-method version of tap().
use Illuminate\Support\Traits\Tappable;class ReportBuilder{ use Tappable;}$builder = new ReportBuilder();$builder = $builder->tap(function (ReportBuilder $instance) { logger()->debug('builder initialized');});
Tappable is useful when you want to insert side effects into a fluent API chain. When combined with Macroable and Conditionable, you can build highly extensible builder APIs in the Laravel style.
1
Create a fluent class
use Illuminate\Support\Traits\Conditionable;use Illuminate\Support\Traits\Macroable;use Illuminate\Support\Traits\Tappable;class QueryPresetBuilder{ use Macroable; use Conditionable; use Tappable; protected array $filters = []; public function where(string $key, mixed $value): static { $this->filters[$key] = $value; return $this; } public function toArray(): array { return $this->filters; }}
Laravel itself uses tap() and Tappable in real code:
Illuminate\Routing\Router uses both Macroable and Tappable
Router::respondWithRoute() uses tap($route)->bind(...) (without a callback), then still returns the original $route
Router::prepareResponse() uses tap(..., fn (...) => event(...)) to dispatch an event after response conversion
Illuminate\Testing\Fluent\Concerns\Has implements assertion helpers with a ->tap(...)->first(...)->etc() chain
// From Illuminate\Routing\Router$route = tap($this->routes->getByName($name))->bind($this->currentRequest);return tap(static::toResponse($request, $response), function ($response) use ($request) { $this->events->dispatch(new ResponsePrepared($request, $response));});
tap() is a small helper by itself, but when you combine it with Macroable and Conditionable, it becomes much easier to build the readable method chains you often see in Laravel.