Ability to use function callbacks comes very handy in many everyday programming situations. With anonymous functions it is possible to inject your own code into another function's execution, which greatly improves code reuse.
These are some simple, but highly practical examples of using callbacks for various programming tasks. In each example, we are going to define a helper function that accepts another callback function as an argument and does some processing with it. The helper function can then be reused by calling it repeatedly with custom callback arguments.
Example1: Time measurement
You have probably already used microtime()
function for benchmarking your program. Now you can define runWithTimer()
helper function, which performs microtime()
calls and time subtractions automatically for you. Code intended for time measurement is passed in a callback parameter $cb
and called from inside of the helper function.
Helper function
function runWithTimer(callable $cb, &$seconds) {
$t0 = microtime(true);
try {
$res = $cb();
return $res;
}
finally {
$t1 = microtime(true);
$seconds = $t1 - $t0;
}
}
The helper function uses &$seconds
parameter passed by reference to store the resulting number of seconds. Helper function itself returns the return value of the callback function $cb
. It could also be implemented the other way round (helper function returning number of seconds and storing callback function return value in an parameter passed by reference).
Usage
$seconds = 0;
$res = runWithTimer(function () {
sleep(3);
return true;
}, $seconds);
var_dump($seconds, $res);
// Outputs: float(3.022805929184) bool(true)
Number of seconds the callback function was executing was stored into $seconds
variable. Callback function itself returned TRUE
, which was stored into the $res
variable.
Note that doing the final time subtraction in finally
block ensures that we get the resulting number of seconds even if the callback function throws an exception.
$seconds = 0;
try {
$res = runWithTimer(function () {
sleep(3);
throw new Exception();
}, $seconds);
}
catch (Throwable $ex) { };
var_dump($seconds, $res);
// Outputs: float(3.0143301486969) NULL
Example 2: Output buffering
Some of your existing functions may be echoing output instead of returning it as a string. Ideally, you should rewrite those functions to return strings, but as a fast workaround, you can use output buffering. runWithOutputBuffer()
helper function encapsulates necessary output buffering calls for you.
Helper function
function runWithOutputBuffer($callback, &$output) {
ob_start();
try {
$res = $callback();
return $res;
}
finally {
$output = ob_get_contents();
ob_end_clean();
}
}
Usage
$output = "";
$res = runWithOutputBuffer(function () {
echo "Hello world!";
return true;
}, $output);
var_dump($output, $res);
// Outputs: string(12) "Hello world!" bool(true)
Here, output of callback function was stored as string into $output
variable. Callback function itself returned TRUE
, which was stored into the $res
variable.
You could also use string-based callback to easily store output of existing echoing function.
$output = "";
runWithOutputBuffer("phpinfo", $output);
var_dump($output);
Example 3: Database transactions
runInTransaction()
function stores repetitive code for committing and rolling back database transactions.
Helper function
function runInTransaction(mysqli $mysqli, callable $cb) {
$mysqli->begin_transaction();
try {
$res = $cb();
$mysqli->commit();
return $res;
}
catch (Throwable $ex) {
$mysqli->rollback();
throw $ex;
}
}
The helper function executes callback $cb
and invokes transaction rollback if the callback throws an exception. Otherwise, it commits the transaction. (Callback $cb
should contain database data manipulation logic, otherwise it does not make sense to pass it to the runInTransaction()
function.)
NOTE: In a real project code you would probably implement the helper function as a method of your database connection provider class, so that the $mysqli
object could be stored in a class variable instead of being repeatedly passed through function parameter.
Usage
try {
$persons = [
["id" => 1, "name" => "John"],
["id" => 2, "name" => "Monica"],
];
$countInserted = runInTransaction($mysqli, function () use ($persons) {
$count = 0;
$res = insertIntoTable("persons1", $persons);
if ($res === false) throw new Exception;
else $count += $res;
$res = insertIntoTable("persons2", $persons);
if ($res === false) throw new Exception;
else $count += $res;
return $count;
});
echo "Transaction commited ($countInserted rows inserted).";
}
catch (Throwable $ex) {
echo "Transaction rolled back.";
}
In this example, callback represented by anonymous function inserts data into two different database tables. Database manipulation logic is black-boxed in the insertIntoTable()
function. When a database error is signaled by a FALSE
return value, callback function throws an exception, and thus causes transaction rollback. Otherwise, it returns number of inserted rows, which is finally stored in $coutInserted
variable.
Since $persons
data intended for database insertion were defined outside the scope of the anonymous function, we have applied use
construct in anonymous function definition to bring $perons
variable into the inner scope (function () use ($persons)
).
NOTE: You could use similar helper functions even for other database-related tasks, like executing callback functions against certain default database (you would probably use $mysqli->select_db(...)
in the helper function code).
Example 4: Caching
Our final example deals with caching of function return values. Usually you would want to cache resource-consuming calculations, database querying functions or other I/O related calls.
Similar to previously mentioned helper functions, createCache()
accepts callback function in $cb
parameter. This callback function represents a computation for which we want to cache return values. Helper function finally returns a completely new anonymous function, which can then be used by outer code as a caching function.
Helper function
function createCache(callable $cb, bool $multiArgs = false) {
$cache = [];
if (!$multiArgs) {
return function ($argument) use (&$cache, $cb) {
if (array_key_exists($argument, $cache)) return $cache[$argument];
else {
$cache[$argument] = $cb($argument);
return $cache[$argument];
}
};
}
else {
return function (...$args) use (&$cache, $cb) {
$valFound = false;
foreach ($cache as list($args1, $val1)) {
if ($args1 === $args) {
$val = $val1;
$valFound = true;
break;
}
}
if ($valFound) return $val;
else {
$val = $cb(...$args);
$cache[] = [$args, $val];
return $val;
}
};
}
};
Helper function first creates $cache
array, which will serve as a storage for cached return values. Then it returns an anonymous function which executes a given callback $cb
and caches its return value, but only if it does not find its return value already stored in the cache. Returned anonymous function could have been created in two branches, which is only a performance tweak - return values of callback functions accepting only one argument can be cached more efficiently, which is what the first branch is for.
Usage
We will define getPerson()
function querying person's data from the database and then we create caching function $getPersonCached()
from it.
function getPerson ($id, $nameOnly = false) {
echo "called widh id $id<br />";
$person = selectFromTable("persons", $id);
return $nameOnly ? $person["name"] : $person;
}
$getPersonCached = createCache("getPerson", true);
$person1 = $getPersonCached("1");
$person2 = $getPersonCached("2");
$person1 = $getPersonCached("1");
// Outputs:
// called with id 1
// called with id 2
As you see, repeated call to $getPersonCached()
with argument "1"
hasn't invoked getPerson()
function, because the return value has been obtained from the cache.
If you wanted to make calls to getPerson()
with the optional parameter $nameOnly
changing, you would have to call createCache()
with parameter $multiArgs
set to TRUE
.
$getPersonCache = createCache("getPerson", true);
$person1 = $getPersonCache("1");
$personName1 = $getPersonCache("1", true);
NOTE: We have used string-based callback "getPerson"
as an argument for createCache()
function in this examples, but you could naturally use anonymous function callbacks as well.
Conclusion
I hope this examples helped you to understand how to build better code. They also serve as an introduction to functional programming in PHP.
For advanced techniques, see following sections of PHP Manual:
Image credit: https://commons.wikimedia.org/wiki/File:Php_elephant_logo.svg
Top comments (2)
I thought this would be a beginner friendly article. Anyway, it was nice reading your writing. Good day!
I never thought of using try-finally as a means to get PHP to do something after returning from a function, you really opened my eyes on this. Not too sure if this is good practice, but it certainly seems to be a great way to accomplish some things, like in your examples. I liked the usage with output buffering.