Introducing Laravel’s tap, “higher order” tap, and collection tap
!Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.
If you follow Adam Wathan at all, you’ve seen him use his favorite function, tap()
, dozens of times in his videos. But what does it actually do?
tap()
was introduced in Laravel 5.3 and got some power boosts in 5.4. Let’s take a look at what it is and what it does.
Defining tap()
function tap($value, $callback)
{
$callback($value);
return $value;
}
That’s what the tap()
function originally looked like—it's gotten a few small improvements since then, but let's start at the basics.
It’s really simple, right? So why is everyone so excited?
Let’s look at what it does: Take a value, pass that value into some form of callback, and then return the value.
An example using tap()
Here’s what it looks like to refactor something to use tap()
.
First, let’s take a common pattern:
public function generateUseAndReturnThing($input)
{
$thing = $this->thingFromInput($input);
$this->doActionToThing($thing);
return $thing;
}
We take some input, make a thing from it, perform some actions with that thing, and then finally return the thing that we generated.
So, remember, tap()
takes a value, acts on it, and then returns it. That means we can identify a potential place to use tap()
here; we’re acting on a value ($thing
) and then returning that same value.
That’s our key to watch out for. Consider that a code smell that hints at a potential use for tap()
usage: using temporary variables just for the sake of returning later.
So, let’s make this use tap()
instead.
public function generateUseAndReturnThing($input)
{
return tap($this->thingFromInput($input), function ($thing) {
$this->doActionToThing($thing);
});
}
Let’s walk through what we’ve changed here.
First, we are no longer assigning the output of $this->thingFromInput($input)
to a temporary variable; we’re instead passing it directly as the “value” to tap()
.
Second, we know the “value” (that is, the first parameter you send to tap()
) is provided as a parameter to our callback function, so we pass in a closure that takes the “value” $thing
as its parameter.
Third, we operate on our value.
And fourth, we close out our tap function. The final return of the tap()
function is the originally-passed value (the output of $this->thingFromInput($input)
) so this is what is returned from our generateUseAndReturnThing()
method.
Evaluating the refactor
How much did that help? What did it bring us?
Before we ask those questions, let’s read a few words from a recent post by Taylor Otwell about Tap (Tap, Tap, Tap). Remember also that Taylor and Adam Wathan are the ones who originally popularized tap()
—think of their influences and values.
On first glance, this Ruby inspired function is pretty odd. … I find it often lets me write terse, one-line operations that would normally require temporary variables or additional lines.
This isn’t allowing us to do things we couldn’t do before.
Instead, it’s helping us write terser code, with less temporary variables and less lines of code.
A few real life examples
In Taylor’s article he gave an example of how he’s using Tap in the Laravel core.
Eloquent create
public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}
This is the create()
method on Eloquent models. To understand its value, let’s take a look at what this same method call would look like without tap()
:
public function create(array $attributes = [])
{
$instance = $this->newModelInstance($attribtues);
$instance->save();
return $instance;
}
We’re dropping the need for the $instance
temporary variable and wrapping the instantiation, save()
call, and the return of the instance itself up into the core workflow of the tap()
function.
Decorate response middleware
Here’s another really practical use case. One of the most common workflows for writing middleware in Laravel looks a bit like this:
public function handle($request, Closure $next)
{
$response = $next($request);
$this->decorateResponseSomehow($response);
return $response;
}
We can now refactor this using tap()
:
public function handle($request, Closure $next)
{
return tap($next($request), function ($response) {
$this->decorateResponseSomehow($response);
});
}
Shortcut without affecting the value
There’s another interesting use case that I came across in Derek MacDonald’s article on Tap; Derek found that the AuthenticateSession
middleware now uses tap()
, but not quite the way I described it above.
public function handle($request, Closure $next)
{
return tap($next($request), function () use ($request) {
$this->storePasswordHashInSession($request);
});
}
Notice anything different? Here’s what that call would look like before tap()
:
public function handle($request, Closure $next)
{
$response = $next($request);
$this->storePasswordHashInSession($request);
return $response;
}
Unlike in my example, the Closure isn’t actually performing any operations working with the “value” (the result of $next($request)
). Instead, this method uses tap
because it still saves us from needing to save $response
as a temporary variable before storing the hash.
“Higher order” tap
In Laravel 5.4, the tap()
function got a new use case. Most of the examples I’ve shown up until this point have been calling an outside method on our value:
return tap($thing, function ($thing) {
$this->doSomethingToThing($thing);
});
But there are other times where you want to call a method on the object itself; in these cases, the benefit of using tap()
is just to return the object when the method we're calling on the object would normally return something else—for example, Eloquent models' update()
method returns a boolean.
That converts this call:
$user->update([
'name' => $name,
'age' => $age,
]);
return $user;
into this:
return tap($user, function ($user) {
$user->update([
'name' => $name,
'age' => $age,
]);
});
As you can see, this isn’t quite as much of an improvement as our previous refactorings-with-tap
were. But in 5.4, you can now use tap()
to call methods on the tapped object, and those methods—even if they natively return something other than the object—will return the object. Here’s how:
return tap($user)->update([
'name' => $name,
'age' => $age,
]);
Now that’s clean.
Collection tap in Laravel 5.4
In Laravel 5.4, we got the tap()
method in our collections as well. Using tap()
inline temporarily pauses your collection pipeline and passes you an instance of the collection itself, but once your tap
Closure has executed, the pipeline continues as if nothing had happened.
Note that whatever you return
from your tap
method is just thrown away. This isn’t for returns; it’s for debugging with var_dump
or for writing to a log or for performing some separate action.
return collect($peopleArray)
->sortBy('name')
->tap(function ($people) {
// Useful for debugging
var_dump($people);
})
->filter(function ($person) {
return $person->syncable === true;
})
->tap(function ($people) {
// Useful for performing some operation without
// requiring a temporary variable
app('thirdPartyService')->syncPeople($people);
});
My first response was to compare this method to pipe()
and each()
, but they’re different in that each()
is passed each of the items in the collection one at a time where tap()
is passed the entire collection; and pipe()
modifies the collection to be whatever you return from the method, whereas tap()
discards your return.
Tap to the end
I’m a huge fan of anything that makes our code simpler to read for developers down the road. I’m also a huge fan of coding practices that make our code more expressive and that avoid unnecessary extra code.
tap()
, and collection pipelines before it, are strange in that, when they’re first introduced to your programming lexicon, there’s a cost; until you wrap your head around how tap()
works, it’s actually more cognitive work than not using tap()
. Same with collection pipelines.
Just like collection pipelines, I’ve also seen people get so excited about tap()
that they use it everywhere—often in places where it doesn’t belong. Just because something is new, doesn’t mean you should use it everywhere.
All those caveats having been given, though, I do think tap()
has a place in our programming vocabulary, and I’m glad it’s here to stay. Just search for tap()
in the Laravel codebase and you can see how many places Taylor and others are putting tap()
to good use.
I hope this longer writeup of tap()
has helped it really click in your brain. Let me know on Twitter if anything in this article isn't clear.
Comments? I'm @stauffermatt on Twitter
Tags: laravel • tap • collections