Laravel 5: Execute a Callback on a Given Value While Chaining the Original Value With tap

April 15, 2018 —John Koster

#Signature

The signature of the tap function is:

1function tap(
2 $value,
3 $callback = null
4);

#Example Use

The tap helper function is used to call a given Closure (provided as an argument to the $callback parameter) with the given $value. The tap helper function will return the reference to the original $value as its return value. This allows you to call any number of methods with any type of return values within the $callback while maintaining a reference to the original object. This behavior is particularly useful during method chaining. Another way to think of this functions behavior is that it allows you to specify and fix the return value of a series of method calls, regardless of what methods are called within the internal block.

The tap helper function was added in Laravel version 5.3 and defined in the src/Illuminate/Support/helpers.php file. This function can easily be integrated into older Laravel code bases as it does not depend on any new fundamental framework components.

The following example PHP classes will be used to demonstrate a trivial usage of the tap helper function. The classes represent a crude "talker" system where one can add messages and retrieve the final message back:

In app/Message.php:

1<?php
2 
3namespace App;
4 
5class Message
6{
7 
8 /**
9 * The actual message.
10 *
11 * @var string
12 */
13 protected $message = '';
14 
15 /**
16 * Gets the original message.
17 *
18 * @return string
19 */
20 public function getOriginal()
21 {
22 return $this->message;
23 }
24 
25 /**
26 * Changes the message.
27 *
28 * @param $message
29 * @return Message
30 */
31 public function change($message)
32 {
33 $this->message = $message;
34 
35 return $this;
36 }
37 
38 public function __toString()
39 {
40 return strtolower($this->message);
41 }
42 
43}

The Message class if fairly simple. It is simply a value object that wraps PHP's native string data type. It provides a few methods that can be used to retrieve the original value and change (mutate) the internal value of the message. When the message is cast back to a string it is converted to its lower cased variant. The important thing to notice is that the change method returns a reference back to itself.

In app/Talker.php:

1<?php
2 
3namespace App;
4 
5class Talker
6{
7 
8 /**
9 * The actual parts of the message.
10 *
11 * @var array
12 */
13 protected $messageParts = [];
14 
15 /**
16 * Adds a message part to the end of the message.
17 *
18 * @param $message
19 * @return Message
20 */
21 public function atEnd(Message $message)
22 {
23 array_push($this->messageParts, $message);
24 return $message;
25 }
26 
27 /**
28 * Adds a message part to the start of the message.
29 *
30 * @param $message
31 * @return Message
32 */
33 public function atBeginning(Message $message)
34 {
35 array_unshift($this->messageParts, $message);
36 return $message;
37 }
38 
39 /**
40 * Returns the string representation of the message.
41 *
42 * @return string
43 */
44 public function talk()
45 {
46 return ucfirst(
47 implode(' ', $this->messageParts)
48 );
49 }
50 
51}

The Talker class is also very simple. It maintains an array of Message instances and allows you to add them to either the beginning or ending of the array of messages. In addition, it allows you to retrieve a string made up of all the individual messages with the first character upper cased. Annoyingly the atEnd and atBeginning methods return the Message instance instead of the Talker instance, which makes chaining rather difficult.

The following example will create a simple helper function to remove some of the steps of using the Talker and Message APIs:

1<?php
2 
3use App\Talker;
4use App\Message;
5 
6/**
7 * Say something.
8 *
9 * string $message
10 * @return string
11 */
12function saySomething($message) {
13 $talker = new Talker;
14 $talker->atBeginning(new Message)->change($message);
15 return $talker->talk();
16}

Using the new helper function might look like this:

1<?php
2 
3echo saySomething('Hello World');

The user would see the following text in the browser:

1Hello world

It would be nice if we could return the value of the method chain from our function like so:

1<?php
2 
3// ...
4 
5function saySomething($message) {
6 $talker = new Talker;
7 return $talker->
8 atBeginning(new Message)
9 ->change($message)->talk();
10}
11 
12// ...

However, that code would not work. This is because the atBeginning method on the Talker class returns an instance of Message. The talk method does not exist on the Message class, and if it did would probably not have the same behavior as the Talker instance method. We can use the tap function to get around this quite easily:

1<?php
2 
3// ...
4 
5function saySomething($message) {
6 $talker = new Talker;
7 return tap($talker, function($t) use ($message) {
8 $t->atBeginning(new Message)->change($message);
9 })->talk();
10}
11 
12// ...
Tap Function Closure Arguments

In the previous example the Closure defines a parameter named $t. When the tap function executes, the value of $talker will be used as the inner value of $t and they will refer to the same Talker instance. The following diagram visually demonstrates the flow of data in the tap helper function.

In the above example we are using the tap function to keep our method chain going. In addition we are passing the $message parameter into the inner callback function so that we can use it. The above example could actually be simplified further by using the with helper function to return a new instance of Talker:

1<?php
2 
3// ...
4 
5function saySomething($message) {
6 return tap(with(new Talker), function($t) use ($message) {
7 $t->atBeginning(new Message)->change($message);
8 })->talk();
9}
10 
11// ...

The previous example is very contrived but does demonstrate a fairly basic use of the tap helper function. More common use cases might be when passing around Eloquent models with extensive method chaining.

The following example will make use of the Eloquent model factory builder to create a new instance, modify an attribute, save the model and then return the created model instance:

1<?php
2 
3use App\User;
4 
5$user = tap(factory(User::class)->make(), function($user) {
6 $user->setAttribute('name', 'Not such a random name')
7 ->save();
8});

After the above example has executed, the attributes of the $user model instance might look similar to the following output:

1array [
2 "name" => "Not such a random name"
3 "email" => "Batz.Bridget@example.com"
4 "password" => "$2y$10$aXsXCEx3pkgm..."
5 "remember_token" => "nayR2BKwaQ"
6 "updated_at" => "2016-06-03 22:29:01"
7 "created_at" => "2016-06-03 22:29:01"
8 "id" => 7
9 ]

The actual attribute values will differ as they are randomly assigned a value from the model factory. Because of the way we used the tap helper function we were able to modify the name attribute elegantly, save the model and get the exact return value we wanted.

#Using tap to Proxy Method Calls

If the $callback parameter is omitted when invoking the tap helper function, it will allow you to proxy method calls to the provided value. This is different from the with helper function; instead of returning the return value of the method call, the tap function will return the original $value.

Imagine we had a class that required setup before it can be used, such as initializing a connection. Imagine we were interacting with a class like the example DatabaseConnection class below:

1class DatabaseConnection {
2 
3 /**
4 * The PDO Connection.
5 *
6 * @var PDO
7 */
8 protected $connection;
9 
10 /**
11 * Establishes a connection with a database server.
12 *
13 * @param string $connectionString
14 * @param string $userName
15 * @param string $password
16 */
17 public function connect(
18 $connectionString,
19 $userName,
20 $password
21 )
22 {
23 $this->connection = new PDO(
24 $connectionString,
25 $userName,
26 $password
27 );
28 }
29 
30 /**
31 * Executes an SQL statement, returning a result set.
32 *
33 * @param string $query
34 * @return PDOStatement
35 */
36 public function query($query)
37 {
38 return $this->connection->query($query);
39 }
40 
41 /**
42 * Closes the existing database server connection.
43 */
44 public function close()
45 {
46 $this->connection = null;
47 }
48 
49}

The designers of the DatabaseConnection class did not make their API fluent, meaning that we cannot instantiate the database connection and connect to the database server in one step; our code might look similar to the following:

1$connection = new DatabaseConnection;
2$connection->connect(
3 'mysql:host=localhost;dbname=forge',
4 'username',
5 'password'
6 );
7 
8// Interact with the database server.

By leveraging method proxies using the tap function, we could rewrite that code to the following:

1$connection = tap(new DatabaseConnection)->connect(
2 'mysql:host=localhost;dbname=forge',
3 'username',
4 'password'
5 );
6 
7// Interact with the database server.

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.