PHP Callback Functions
What Is a Callback? In simple terms: A callback is a function that you hand to another function, so that the other function can call it back at the right moment. You decide what should happen; the rec
What Is a Callback?
In simple terms: A callback is a function that you hand to another function, so that the other function can call it back at the right moment. You decide what should happen; the receiving function decides when it happens.
array_map('strtoupper', ['php', 'mysql']); // array_map calls strtoupper for each element
Technically: In PHP, anything that can be called is a callable. A parameter declared with the callable type (or Closure) accepts such values, and the receiving code invokes them with $callback(...$arguments). Functions that accept or return other functions are called higher-order functions.
Why Callbacks Are Useful
Callbacks separate a general algorithm from the specific behavior:
usort()knows how to sort; your callback says how to compare two items.array_filter()knows how to loop; your callback says which items to keep.- A retry helper knows how to repeat an operation; your callback is the operation.
- An event system knows when something happened; callbacks define what to do about it.
The same general code can then be reused with many different behaviors.
The Forms of Callables
| Form | Example | Notes |
|---|---|---|
| Function name string | 'strtoupper' | Built-in or user-defined function |
| Closure (anonymous function) | function ($x) { return $x * 2; } | See Anonymous Functions |
| Arrow function | fn($x) => $x * 2 | See Arrow Functions |
| First-class callable (PHP 8.1+) | strtoupper(...), $obj->method(...) | See First-class Callable Syntax |
| Object method array | [$object, 'method'] | Older style |
| Static method | [ClassName::class, 'method'] or 'ClassName::method' | Older style |
| Invokable object | An object with an __invoke() method | Covered below |
Example: Each Form in Action
<?php
class Greeter
{
public function greet(string $name): string
{
return "Hello, $name";
}
public static function shout(string $name): string
{
return strtoupper("hi, $name!");
}
}
$greeter = new Greeter();
$callables = [
'string' => 'ucfirst',
'closure' => function (string $name): string { return "Welcome, $name"; },
'arrow' => fn(string $name): string => "Vanakkam, $name",
'first-class' => $greeter->greet(...),
'array' => [$greeter, 'greet'],
'static' => [Greeter::class, 'shout'],
];
foreach ($callables as $form => $callable) {
echo str_pad($form, 12) . $callable('asha') . "\n";
}
Output
string Asha
closure Welcome, asha
arrow Vanakkam, asha
first-class Hello, asha
array Hello, asha
static HI, ASHA!
Every form is called the same way: $callable('asha'). Code receiving a callback does not need to know which form it was given.
Writing Functions That Accept Callbacks
Example: Transforming With a Callback
<?php
function transformAll(array $items, callable $transform): array
{
$result = [];
foreach ($items as $key => $item) {
$result[$key] = $transform($item);
}
return $result;
}
$prices = ['Pen' => 20, 'Lamp' => 1299];
print_r(transformAll($prices, fn(int $price): string => '₹' . number_format($price * 1.18, 2)));
Output
Array
(
[Pen] => ₹23.60
[Lamp] => ₹1,532.82
)
How It Works
callable $transformdeclares that the second argument must be callable; passing anything else causes aTypeError.- Inside the loop,
$transform($item)calls whatever function the caller provided. transformAll()handles the looping and key preservation; the caller supplies only the transformation.
callable vs Closure Type Declarations
| Type | Accepts | Notes |
|---|---|---|
callable | All forms in the table above | Cannot be used for property types |
Closure | Closures, arrow functions, first-class callables | Can be used for properties; convert other forms with Closure::fromCallable() or (...) |
Passing Extra Data to Callbacks
A callback receives only the arguments the calling code passes. To give it additional data, capture variables in a closure:
<?php
$students = [
['name' => 'Asha', 'score' => 88],
['name' => 'Bala', 'score' => 34],
['name' => 'Charu', 'score' => 61],
];
$passMark = 40;
$passed = array_filter($students, fn(array $student): bool => $student['score'] >= $passMark);
echo implode(', ', array_column($passed, 'name')) . "\n";
Output
Asha, Charu
array_filter() calls the callback with one argument (the student), but the arrow function also uses $passMark from the surrounding scope. This is the standard way to "configure" a callback.
Practical Callback Patterns
Retrying an Operation
<?php
function retry(callable $operation, int $maxAttempts, ?callable $onFailure = null): mixed
{
for ($attempt = 1; ; $attempt++) {
try {
return $operation($attempt);
} catch (RuntimeException $e) {
if ($onFailure !== null) {
$onFailure($e, $attempt);
}
if ($attempt >= $maxAttempts) {
throw $e;
}
}
}
}
$result = retry(
function (int $attempt): string {
if ($attempt < 3) {
throw new RuntimeException("Gateway timeout");
}
return "Payment confirmed on attempt $attempt";
},
5,
fn(RuntimeException $e, int $attempt) => print("Attempt $attempt failed: {$e->getMessage()}\n")
);
echo $result . "\n";
Output
Attempt 1 failed: Gateway timeout
Attempt 2 failed: Gateway timeout
Payment confirmed on attempt 3
retry() contains the general logic (counting attempts, catching failures, giving up); the operation and the optional failure handler are callbacks. The same helper can retry API calls, database connections, or file uploads.
Measuring Execution Time
<?php
function timed(string $label, callable $task): mixed
{
$start = hrtime(true);
try {
return $task();
} finally {
printf("%s took %.1f ms\n", $label, (hrtime(true) - $start) / 1e6);
}
}
$sum = timed('Summing', fn(): int => array_sum(range(1, 100_000)));
echo $sum . "\n";
Output (the time varies)
Summing took 1.2 ms
5000050000
The finally block prints the time even if the task throws an exception. Wrapping code in callbacks like this adds behavior (timing, logging, transactions) around any operation without modifying it.
Invokable Objects: __invoke()
An object whose class defines __invoke() can be called like a function. This is useful when a callback needs configuration or dependencies:
<?php
final class DiscountCalculator
{
public function __construct(private float $percent) {}
public function __invoke(float $price): float
{
return round($price * (1 - $this->percent / 100), 2);
}
}
$festivalDiscount = new DiscountCalculator(15);
echo $festivalDiscount(1000) . "\n";
print_r(array_map($festivalDiscount, [499, 1299]));
var_dump(is_callable($festivalDiscount));
Output
850
Array
(
[0] => 424.15
[1] => 1104.15
)
bool(true)
The object carries its configuration (15%) and can be passed anywhere a callable is expected. Unlike a closure, it has a name, can be type-hinted by class, and can be tested and reused independently.
Practical Example: A Simple Event Hook System
<?php
final class Hooks
{
private array $listeners = [];
public function on(string $event, callable $listener, int $priority = 10): void
{
$this->listeners[$event][$priority][] = $listener;
}
public function trigger(string $event, mixed ...$args): void
{
$byPriority = $this->listeners[$event] ?? [];
ksort($byPriority);
foreach ($byPriority as $listeners) {
foreach ($listeners as $listener) {
$listener(...$args);
}
}
}
}
$hooks = new Hooks();
$hooks->on('order.paid', fn(array $order) => print("Email receipt to {$order['email']}\n"));
$hooks->on('order.paid', fn(array $order) => print("Reserve stock for order #{$order['id']}\n"), priority: 1);
$hooks->on('order.paid', fn(array $order) => print("Notify warehouse\n"), priority: 20);
$hooks->trigger('order.paid', ['id' => 501, 'email' => 'asha@example.com']);
$hooks->trigger('order.refunded', ['id' => 502]); // no listeners: nothing happens
Output
Reserve stock for order #501
Email receipt to asha@example.com
Notify warehouse
Step-by-Step Explanation
on()stores callbacks grouped by event name and priority.trigger()sorts the priorities (lower numbers run first) and calls each listener with the event's arguments.- The order-processing code only triggers
order.paid; it does not need to know which actions follow. New behavior is added by registering another callback.
This is the core idea behind WordPress actions and filters, Laravel events, and Symfony's EventDispatcher.
Common Mistakes
Passing the Result Instead of the Function
array_map(strtoupper('php'), $items); // wrong: passes the string "PHP"
array_map('strtoupper', $items); // right: passes the function
Callbacks With the Wrong Number of Parameters
Effect: Built-in functions call callbacks with specific arguments (for example, array_filter() with ARRAY_FILTER_USE_BOTH passes value and key). A callback expecting different parameters fails or behaves unexpectedly.
Fix: Check the documentation for the arguments each function passes.
Using String Callables for Methods
Effect: Typos are found only at runtime, and IDEs cannot follow them.
Fix: Prefer closures and first-class callable syntax.
Hidden Side Effects in Callbacks
Effect: Callbacks in array_map() that modify external state are hard to follow.
Fix: Keep transformation callbacks pure; use explicit loops or event hooks for side effects.
Best Practices
- Use callbacks to separate general algorithms from specific behavior.
- Type-hint callback parameters with
callableorClosure. - Use closures to pass configuration into callbacks.
- Use invokable classes for callbacks that need dependencies or should be reusable and testable.
- Prefer first-class callable syntax over string and array callables in modern code.
Official reference: https://www.php.net/manual/en/language.types.callable.php