Practical use of callback functions in PHP
Tags: php, functional
Published: 2021-03-02 15:21:38


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
```php
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
```php
$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.

```php
$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](https://www.php.net/manual/en/book.outcontrol.php). `runWithOutputBuffer()` helper function encapsulates necessary output buffering calls for you.

### Helper function
```php
function runWithOutputBuffer($callback, &$output) {
  ob_start();
  try { 
    $res = $callback();
    return $res;
  }
  finally {
    $output = ob_get_contents();
    ob_end_clean();
  }
}
```
### Usage

```php
$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.
```php
$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
```php
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
```php
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
```php
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.
```php
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`.
```php
$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:
* [Callbacks](https://www.php.net/manual/en/language.types.callable.php)
* [Anonymous functions](https://www.php.net/manual/en/functions.anonymous.php)
* [Arrow functions](https://www.php.net/manual/en/functions.arrow.php)
* [Generators](https://www.php.net/manual/en/language.generators.overview.php)

*Image credit: https://commons.wikimedia.org/wiki/File:Php_elephant_logo.svg*