Closures and Callables

In PHP, functions can be values: stored in variables, passed to other functions and returned from them. Anonymous functions (closures), short arrow functions, first-class callable syntax and PHP 8.5's pipe operator make this style concise — and it's what powers array_map, usort, routing tables and event handlers.

Anonymous functions

A function without a name can be assigned to a variable and called through it:

PHP
<?php

$square = function (int $x): int {
    return $x * $x;
};

echo $square(7), "\n";
echo get_debug_type($square), "\n";
echo implode(", ", array_map($square, [1, 2, 3])), "\n";
Output
49
Closure
1, 4, 9

Note the semicolon after the closing brace — it's an assignment statement.

Capturing variables with use

An anonymous function can't see outside variables unless it lists them in use. By default it captures a copy of the value at the moment the function is created:

PHP
<?php

$rate = 0.2;
$addTax = function (float $price) use ($rate): float {
    return $price * (1 + $rate);
};

$rate = 0.5;               // changing it later doesn't affect the closure
echo $addTax(100), "\n";

$total = 0;
$add = function (int $n) use (&$total): void {   // capture by reference
    $total += $n;
};
$add(5);
$add(10);
echo $total, "\n";
Output
120
15

Arrow functions

For a single expression, an arrow function is much shorter. It captures outside variables automatically (by value) and returns its expression:

PHP
<?php

$rate = 0.2;
$addTax = fn(float $price): float => $price * (1 + $rate);
echo $addTax(50), "\n";

$products = [["name" => "Lamp", "price" => 34.5], ["name" => "Desk", "price" => 249.0]];
$minPrice = 100;

$expensive = array_filter($products, fn($p) => $p["price"] > $minPrice);
echo implode(", ", array_column($expensive, "name")), "\n";

usort($products, fn($a, $b) => $b["price"] <=> $a["price"]);
echo $products[0]["name"], "\n";
Output
60
Desk
Desk

Arrow functions are the natural choice for callbacks to array functions. Use a full function when you need several statements or want to modify an outside variable.

Callables

Many functions accept a callable: anything PHP can call. That includes closures, function names as strings, and [$object, 'method'] or [ClassName::class, 'method'] arrays:

PHP
<?php

class Formatter
{
    public function shout(string $s): string { return strtoupper($s) . "!"; }
    public static function whisper(string $s): string { return strtolower($s) . "..."; }
}

$words = ["Hello", "World"];
$f = new Formatter();

echo implode(" ", array_map('strrev', $words)), "\n";
echo implode(" ", array_map([$f, 'shout'], $words)), "\n";
echo implode(" ", array_map([Formatter::class, 'whisper'], $words)), "\n";
var_dump(is_callable('strlen'), is_callable('noSuchFunction'));
Output
olleH dlroW
HELLO! WORLD!
hello... world...
bool(true)
bool(false)

First-class callable syntax

Since PHP 8.1, writing a function or method call with (...) instead of arguments creates a closure from it. It's clearer than strings and arrays, and editors and static analysis can check it:

PHP
<?php

class Formatter
{
    public function shout(string $s): string { return strtoupper($s) . "!"; }
}

$trim = trim(...);
$shout = new Formatter()->shout(...);

echo "[", $trim("  padded  "), "]\n";
echo implode(" ", array_map($shout, ["hi", "there"])), "\n";
echo implode(",", array_map(intval(...), ["1", "2", "3"])), "\n";
Output
[padded]
HI! THERE!
1,2,3

(new Formatter()->shout(...) without extra parentheses around new is allowed since PHP 8.4.)

Functions that return functions

A function can build and return a closure. Each returned closure remembers its own captured values:

PHP
<?php

function multiplier(int $factor): Closure
{
    return fn(int $x): int => $x * $factor;
}

$double = multiplier(2);
$triple = multiplier(3);
echo $double(10), " ", $triple(10), "\n";

function makeCounter(): Closure
{
    $count = 0;
    return function () use (&$count): int {
        return ++$count;
    };
}

$next = makeCounter();
$next();
$next();
echo $next(), "\n";
Output
20 30
3

The pipe operator (PHP 8.5)

|> passes the value on its left into the callable on its right. Chaining pipes lets you write a sequence of transformations top to bottom, instead of nesting calls inside-out:

PHP
<?php

$title = "  PHP 8.5 Brings The Pipe Operator  ";

// nested: read from the inside out
$slug1 = str_replace(' ', '-', strtolower(trim($title)));

// piped: read from top to bottom
$slug2 = $title
    |> trim(...)
    |> strtolower(...)
    |> (fn($s) => str_replace(' ', '-', $s))
    |> (fn($s) => preg_replace('/[^a-z0-9-]/', '', $s));

echo $slug1, "\n";
echo $slug2, "\n";
Output
php-8.5-brings-the-pipe-operator
php-85-brings-the-pipe-operator

Each step must be a callable that takes exactly one argument. Built-in functions with one required parameter work directly via (...); for others, wrap the call in an arrow function — in parentheses, as shown.

Type-hinting callables

Declare parameters as callable (any callable) or Closure (closures only). Here a small event system stores handlers and calls them later:

PHP
<?php

$handlers = [];

function on(array &$handlers, string $event, callable $handler): void
{
    $handlers[$event][] = $handler;
}

function emit(array $handlers, string $event, mixed $data): void
{
    foreach ($handlers[$event] ?? [] as $handler) {
        $handler($data);
    }
}

on($handlers, 'order.paid', fn($id) => print("Email receipt for order $id\n"));
on($handlers, 'order.paid', fn($id) => print("Update stock for order $id\n"));

emit($handlers, 'order.paid', 1001);
emit($handlers, 'order.cancelled', 1002);
Output
Email receipt for order 1001
Update stock for order 1001

Try it yourself

Build a text-cleaning pipeline from a list of closures and apply it with array_reduce.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
[This is messy input]