As yet undocumented, Laravel 5.3 quietly added a new tap() global helper function, improving the framework’s declarative capabilities. It’s a subtle idiom picked up from Ruby and Lodash that allows you to “tap” into a chain.

You will not use this function often. It does take control of stodgy imperative solutions with right-to-left assignments souring solutions in an unpleasant direction. The end result will be saving 2 or 3 lines on each use while making that code read top-down, left-to-right.

tap() to perform intermediate actions

Starting with a simple example I’ve grabbed some code from Laravel’s AuthenticateSession@handle(). Veteran PHP developers are plenty familiar with the imperative solution:

$response = $next($request);

$this->storePasswordHashInSession($request);

return $response;

A Laravel 5.4 declarative refresh?

return tap($next($request), function () use ($request) {
    $this->storePasswordHashInSession($request);
});

Some devs might be cringing as that use syntactical bloat but most of us know what the PHP closure dream is:

return tap($next($request), () -> {
    $this->storePasswordHashInSession($request);
});

Pattern:

// assign object
// make call using object (or not even using that object!)
// return object

tap() to restore state

Eloquent create and update methods support a ['timestamps' => false] option. If this were implemented as a chainable method, the result is fairly readable.

// Method defined in App\Model that App\Message extends.
public function keepingTimestamps(callable $callback)
{
    try {
        $timestamps = $this->timestamps;
        $this->timestamps = false;

        return tap($this, $callback);
    } finally {
        $this->timestamps = $timestamps;
    }
}

Now that Message model object can be chained:


request()->user()->latestMessage->keepingTimestamps(function ($message) {
    $message->markRead(); // updates a 'read_at' timestamp instead
}); // returns the latestMessage

If you recognize this code has the same pattern of DB::transaction(), Laravel 5.4 rewrote the method to use tap().

Pattern:

// capture state
// make call using object
// restore state
// return that object

Collection tap()

Laravel 5.4 also made its Collection class tappable, which will expose the full power of this approach. Now arbitrary code can be run mid-pipeline without having to break the chain. For Laravel 5.3 or below, copy and paste the 5 line method as a Collection macro into your project’s AppServiceProvider@boot().

Here’s one example I used for a Laravel web site supporting both English and French Canadian locales. Instead of hardcoding months into language translation files, I used Carbon for these month of the year dropdown <option>s. Hence this model decorator method:

public function monthOptions()
{
    return collect(range(1, 12))
        ->keyByValue() // custom Collection macro
        ->tap(function () {
            if (App::getLocale() === 'fr') {
                setlocale(LC_TIME, 'fr_CA');
            }
        })
        ->map(function ($month) {
            return sprintf('%02d - %s', $month,
                Carbon::now()->month($month)->formatLocalized('%B'));
        })
        ->tap(function () {
            if (App::getLocale() === 'fr') {
                setlocale(LC_TIME, '');
            }
        });
}

With just one extra language, I’m happy with this solution. For more language support, the set/restore states would be extracted into other methods to make this even more readable.

Collection tap() to handle console command progress bars

This is the most typical use case I’ve had in collections and it makes commands some of the cleanest code in my projects. You can even keep its method chain for the full handle() duration.

public function handle()
{
    Club::findOrFail($this->option('club'))
        ->members()
        ->subscribed()
        ->get()
        ->tap(function ($members) {
            $this->output->progressStart($members->count());
        })
        ->each(function ($member) {
            Mail::to($member)->queue(new Newsletter($member, $this->matchReport());

            $this->output->progressAdvance();
        })
        ->tap(function () {
            $this->output->progressFinish();
        });
}

public function matchReport()
{
     return once(function () {
          return MatchReport::ofRound($this->option('round'))->firstOrFail();
     });
}

Laravel Query Builder tap() macro

Lastly, what if you wanted a progress bar when the eager-loaded Eloquent memory footprint of that collection pipeline is too high? chunk() the result into pages and use a query extension macro to accomplish the same chaining abilities.

public function handle()
{
    Club::findOrFail($this->option('club'))
        ->players()
        ->active() // let's say this scope sorts players to most recently active
        ->with([
            'awards',
            'seasons.matches',
            'teams',
        ])
        ->tap(function ($query) {
            $this->output->progressStart(with(clone $query)->reorder()->count());
        })
        ->chunk(100, function ($players) {
            $players->each(function ($player) {
                with(new PerformanceReport($player))->save();
            });

            $this->output->progressAdvance();
        })
        ->tap(function () {
            $this->output->progressFinish();
        });
}