01 · THE IDEAREAD → TRY → LEARN
Behavior as a value
A higher-order function is a function that takes another function as an argument, returns one, or does both. PHP supports this through callables: references to functions or methods that can be invoked later. When a parameter is declared callable, the caller decides what happens at the point where the function does its work.
You already know the standard examples: array_map() transforms each element, array_filter() keeps some elements, and usort() sorts using your comparison rule. In each case the built-in function owns the loop, and your callback owns the decision.
Ways to create a callable
The manual lists several forms: a Closure object, a string naming a function, an array holding an object or class name plus a method name, and an object that implements __invoke(). For everyday code, closures are the clearest choice. You can write an anonymous function with function () {}, an arrow function with fn() =>, or a first-class callable such as strtoupper(...), which is available since PHP 8.1.
Here is the simplest case: a callback that transforms every element.
$names = ['ada', 'linus', 'grace'];
$upper = array_map(fn(string $name): string => strtoupper($name), $names);
echo json_encode($upper, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";["ADA","LINUS","GRACE"]
Accepting a callable yourself
Your own functions can accept behavior too. Declare the parameter as callable and invoke it like any function. A closure created with function () use ($x) copies the value of $x at creation time. An arrow function captures outer variables by value automatically, but only the ones it uses.
function apply_twice(callable $callback, int $value): int
{
return $callback($callback($value));
}
$step = 3;
$addStep = function (int $number) use ($step): int {
return $number + $step;
};
echo apply_twice($addStep, 10), "\n";
echo apply_twice('abs', -4), "\n";16 4
The second call passes the string 'abs'. That works because a string naming a function is a valid callable, though a closure is usually safer and easier to analyze.
Filtering keeps the keys
A frequent surprise is that array_filter() preserves the original keys. If you remove an element from the middle of a list, the result has a gap. When you encode that array as JSON, it becomes an object rather than a list. Wrap the result in array_values() when you need a clean list again.
$scores = [40, 75, 90, 55];
$passing = array_filter($scores, fn(int $score): bool => $score >= 60);
$list = array_values($passing);
echo json_encode($passing, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
echo json_encode($list, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"1":75,"2":90}
[75,90]
Worked plan for records
Imagine a list of product records, each an array with name, price (in cents) and stock. To produce a sale report, think in two stages:
- Filter: keep only records where
stockis greater than zero. - Map: turn each remaining record into a smaller record with the discounted price.
- Re-index the final result with
array_values().
Each stage is a small, testable callback. The discount percentage is an outer variable, so an arrow function can read it without a use clause. Use intdiv() so the arithmetic stays in integers and rounds down predictably.
Exercise contract
Write price_report(array $items, int $percentOff): array.
- Each item has
name(string),price(int cents) andstock(int). - Skip items whose
stockis 0. - For the others, return
['name' => ..., 'price' => ...], where the new price isintdiv($price * (100 - $percentOff), 100). - Keep the original order and return a list with keys 0, 1, 2 and so on.
- An empty input returns an empty array.
Watch the boundaries: a 0 percent discount leaves prices unchanged, a 100 percent discount produces 0, and a skipped first item must not leave a gap in the keys.
Common mistakes
- Forgetting that
array_filter()keeps keys, which breaks list output. - Using
round()where integer division is required; it returns a float and can round up. - Mutating outer state inside a callback, which makes the transformation hard to reason about.
- Passing a callback that expects different arguments than the caller provides.
Recap
Higher-order functions let one function supply the loop while another supplies the rule. Closures and arrow functions are the most readable callables, array_filter() needs array_values() when you want a list, and small single-purpose callbacks keep record transformations easy to test.
Functions · Arrays · Types
- Use local callbacks to transform records
- Implement and test a deterministic PHP 8.4 function