Lesson 66 of 100
Functions15 min5 checksPHP 8.5

Pass Behavior into Higher-Order Functions

Learn how callables let you hand behavior to functions like array_map and array_filter, then build a small record transformer with local closures.

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.

Example
<?php
$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";
Output
["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.

Example
<?php
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";
Output
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.

Example
<?php
$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";
Output
{"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:

  1. Filter: keep only records where stock is greater than zero.
  2. Map: turn each remaining record into a smaller record with the discounted price.
  3. 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) and stock (int).
  • Skip items whose stock is 0.
  • For the others, return ['name' => ..., 'price' => ...], where the new price is intdiv($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.

Before you start

Functions · Arrays · Types

You’ll practice
  • Use local callbacks to transform records
  • Implement and test a deterministic PHP 8.4 function
Your task

Make price_report() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.