Practical use of callback functions in PHP

martinkordas

Martin Kordas

Posted on March 2, 2021

Practical use of callback functions in PHP

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage

$output = "";
$res = runWithOutputBuffer(function () {
  echo "Hello world!";
  return true;
}, $output);
var_dump($output, $res);
// Outputs: string(12) "Hello world!" bool(true)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.";
}
Enter fullscreen mode Exit fullscreen mode

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;
      }
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
martinkordas
Martin Kordas

Posted on March 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related