Closures and Callables
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05; the partial function application examples were run on the PHP 8.6 release candidate and are marked upcoming in 8.6. Functions are values in PHP: they can be stored in variables, passed to other functions and returned from them. This page covers the ways to create such function values and to call them.
Anonymous functions and use
An anonymous function (closure) is a function without a name, written as an expression. It is an instance of the
final class Closure. A closure does not see the variables of the scope where it is created unless they are
imported with use:
-
use ($x)captures the value of$xat the moment the closure is created; -
use (&$x)captures a reference, so the closure sees later changes and can modify the variable.
<?php
declare(strict_types=1);
$minYear = 1970;
$isModern = function (array $book) use ($minYear): bool {
return $book['year'] >= $minYear;
};
$minYear = 2000; // too late: the closure captured 1970
$books = [['title' => 'Solaris', 'year' => 1961], ['title' => 'Kindred', 'year' => 1979]];
echo implode(', ', array_column(array_filter($books, $isModern), 'title')), "\n"; // Kindred
// Capturing by reference: a counter shared with the closure
$loans = 0;
$lend = function (string $isbn) use (&$loans): string {
$loans++;
return "lent {$isbn}";
};
$lend('978-0-00-000001-1');
$lend('978-0-00-000003-5');
echo $loans, "\n"; // 2
Inside a class method, a closure automatically binds $this (the object the method was called on). Declaring the
closure static prevents this, which avoids keeping the object alive through the closure and makes the intent clear;
static closures cannot use $this.
Source: Anonymous functions
Arrow functions
Arrow functions (7.4), fn (params) => expression, are a compact syntax for closures whose body is a single
expression. They capture every variable they use from the enclosing scope automatically and by value, at creation
time; they cannot capture by reference or modify outer variables. They nest, and inner arrow functions capture through
outer ones.
<?php
declare(strict_types=1);
$discount = 0.1;
$prices = [10.0, 20.0];
$discounted = array_map(fn (float $p): float => round($p * (1 - $discount), 2), $prices);
print_r($discounted); // [9, 18]
$x = 1;
$arrow = fn (): int => $x;
$x = 2;
var_dump($arrow()); // int(1): captured by value when created
// Nested: the inner function sees $factor through the outer one
$multiplier = fn (int $factor): Closure => fn (int $n): int => $n * $factor;
var_dump($multiplier(3)(14)); // int(42)
// Since 8.0, throw is an expression, so it can appear in an arrow function
$require = fn (?string $v): string => $v ?? throw new InvalidArgumentException('missing value');
Use arrow functions for short callbacks; use a full closure when the body needs statements, or the outer variable must be modified.
Source: Arrow Functions
The Closure class
Closure objects have methods to control their binding: the object $this refers to, and the scope (the class
whose private and protected members the closure can access).
| Method | Purpose |
|---|---|
|
return a copy bound to another object and/or class scope |
|
bind temporarily and call in one step (7.0) |
|
turn any callable into a |
|
the currently executing closure, for recursion without a variable (8.5) |
<?php
declare(strict_types=1);
final class Shelf
{
private array $books = ['Solaris', 'Kindred'];
}
// Read a private property by binding a closure to the object's scope (useful in tests and hydrators)
$peek = fn (): array => $this->books;
$bound = Closure::bind($peek, new Shelf(), Shelf::class);
print_r($bound());
$count = function (): int { return count($this->books); };
var_dump($count->call(new Shelf())); // int(2)
// 8.5: recursion inside an anonymous function without capturing itself
$factorial = function (int $n): int {
return $n <= 1 ? 1 : $n * Closure::getCurrent()($n - 1);
};
var_dump($factorial(5)); // int(120)
Binding closures into other classes' scopes bypasses encapsulation; reserve it for infrastructure code (serialisers,
test helpers, frameworks). Since 8.5, binding misuses that used to warn — binding an object to a static closure,
unbinding $this from a closure that uses it, rebinding the scope of a closure created from a method — are
deprecated.
Source: The Closure class, Closure::bind, Closure::call and Closure::getCurrent
First-class callable syntax (PHP 8.1)
Writing a call with ... as its only argument, strlen(...), creates a Closure from any function or method
without calling it. It works for functions, methods, static methods and invokable objects, respects scope (a private
method can be turned into a closure from inside its class) and is checked when written: a typo is an Error
immediately rather than a confusing failure later.
<?php
declare(strict_types=1);
final class Catalogue
{
private array $titles = ['solaris', 'kindred'];
public function titles(): array
{
return array_map($this->format(...), $this->titles); // private method as a callback
}
private function format(string $title): string
{
return ucfirst($title);
}
public static function slug(string $title): string
{
return strtolower(str_replace(' ', '-', $title));
}
}
$upper = strtoupper(...);
echo $upper('kindred'), "\n"; // KINDRED
print_r((new Catalogue())->titles()); // ["Solaris", "Kindred"]
print_r(array_map(Catalogue::slug(...), ['Wild Seed', 'Dawn'])); // ["wild-seed", "dawn"]
var_dump(strlen(...) instanceof Closure); // bool(true)
Compared with the string and array forms ('strtoupper', [$this, 'format']), first-class callables are refactoring
safe, visible to static analysis, and work for private methods passed out of the class.
Source: First class callable syntax
The callable type and the callable forms
The callable type accepts every value PHP can call:
| Form | Example |
|---|---|
a |
|
an object with |
|
a function name string |
|
a static method string |
|
an |
|
a |
|
All forms are called the same way, $callable(...$args); call_user_func() and call_user_func_array() are no longer
needed. is_callable() checks a value, optionally reporting the callable’s name. The callable type cannot be used for
properties and cannot express a signature; a property or parameter of type Closure, documented in PHPDoc as
Closure(string): int, is often the better choice.
<?php
declare(strict_types=1);
final class Slugger
{
public function __invoke(string $s): string { return strtolower(str_replace(' ', '-', $s)); }
}
/** @param callable(string): string $transform */
function applyToTitles(array $titles, callable $transform): array
{
return array_map($transform, $titles);
}
$titles = ['Wild Seed', 'Dawn'];
print_r(applyToTitles($titles, new Slugger()));
print_r(applyToTitles($titles, 'strrev'));
var_dump(is_callable([new Slugger(), '__invoke'], false, $name), $name); // bool(true) "Slugger::__invoke"
Source: Callbacks / Callables and __invoke()
Closures in constant expressions (PHP 8.5)
Since PHP 8.5, static closures and first-class callables can appear in constant expressions: global and class
constants, parameter defaults, property defaults, static variable initialisers and attribute arguments. This lets
attributes and configuration constants carry behaviour, for example a validation rule or a default formatter.
<?php
declare(strict_types=1);
const UPPER = static function (string $s): string { return strtoupper($s); };
final class Formats
{
public const LENGTH = strlen(...); // first-class callable in a class constant
}
function render(string $title, Closure $format = UPPER): string // closure as a parameter default
{
return $format($title);
}
echo render('kindred'), ' ', (Formats::LENGTH)('kindred'), "\n"; // KINDRED 7
Closures in constant expressions must be static (they cannot use $this) and cannot capture variables with use.
Composing functions with the pipe operator
The pipe operator |> (8.5) passes a value through a sequence of single-argument callables, which turns function
composition into a readable top-to-bottom pipeline (the operator itself is described in
Operators and Expressions). Combined with closures that
return closures, it builds reusable steps:
<?php
declare(strict_types=1);
$keepFrom = fn (int $year): Closure => fn (array $books): array =>
array_filter($books, fn (array $b): bool => $b['year'] >= $year);
$sortBy = fn (string $key): Closure => function (array $books) use ($key): array {
usort($books, fn (array $a, array $b): int => $a[$key] <=> $b[$key]);
return $books;
};
$titles = fn (array $books): array => array_column($books, 'title');
$books = [
['title' => 'Imago', 'year' => 1989],
['title' => 'Solaris', 'year' => 1961],
['title' => 'Kindred', 'year' => 1979],
];
$result = $books
|> $keepFrom(1970)
|> $sortBy('year')
|> $titles
|> (fn (array $t): string => implode(', ', $t));
echo $result, "\n"; // Kindred, Imago
Source: Functional Operators
Partial function application (upcoming in PHP 8.6)
PHP 8.6 adds partial function application: calling a function with ? placeholders (and optionally ... for "all
remaining parameters") does not call it, but returns a Closure that takes the missing arguments. It removes most of
the small arrow functions that the pipe operator otherwise needs. The following was run on PHP 8.6.0RC2:
<?php
declare(strict_types=1);
// requires PHP 8.6
$splitLines = explode("\n", ?); // Closure(string): array
print_r($splitLines("Solaris\nKindred"));
$pad = str_pad(?, 10, '.'); // fix every argument except the first
echo $pad('Dawn'), "|\n"; // Dawn......|
$replaceIn = str_replace(?, ?, 'The Left Hand of Darkness');
echo $replaceIn('Darkness', 'Night'), "\n"; // The Left Hand of Night
// With the pipe operator: no wrapping arrow functions needed
echo "Solaris\nKindred\nDawn" |> explode("\n", ?) |> count(...), "\n"; // 3
Until 8.6 is released (scheduled for 2026-11-19) and adopted, write fn (string $s): array => explode("\n", $s).
Functional idioms over arrays and iterables
PHP’s array functions take callbacks first (array_map, usort) or last (array_filter, array_reduce), a
historical inconsistency that named arguments help with. For large or streaming data, generators give lazy versions
of map and filter that never build intermediate arrays (see
Iterators and Generators):
<?php
declare(strict_types=1);
/** @template T @param iterable<T> $items @param callable(T): bool $keep @return Generator<T> */
function lazyFilter(iterable $items, callable $keep): Generator
{
foreach ($items as $key => $item) {
if ($keep($item)) {
yield $key => $item;
}
}
}
/** @param iterable<mixed> $items */
function lazyMap(iterable $items, callable $fn): Generator
{
foreach ($items as $key => $item) {
yield $key => $fn($item);
}
}
$isbns = (function (): Generator { // imagine millions of lines from a file
yield '978-0-00-000001-1';
yield '979-0-00-000009-9';
yield '978-0-00-000003-5';
})();
$result = lazyMap(
lazyFilter($isbns, fn (string $i): bool => str_starts_with($i, '978')),
fn (string $i): string => str_replace('-', '', $i), // PHP 8.6: str_replace('-', '', ?)
);
foreach ($result as $isbn) {
echo $isbn, "\n";
}
A first-class callable (str_replace(...)) cannot pre-fill arguments, which is why the mapping step is an arrow
function; partial application in PHP 8.6 will shorten it to str_replace('-', '', ?).
Source: Generators overview and array_map
Common pitfalls
-
usecaptures by value at creation: later changes to the variable are not seen, unless&is used. -
Arrow functions cannot modify outer variables: assignments inside them change a local copy.
-
Non-static closures in classes keep
$thisalive: mark long-lived callbacksstatic. -
String callables are not checked until called:
'strtoupperr'fails at runtime;strtoupperr(...)fails where it is written. -
Arrow functions in pipes need parentheses:
$x |> (fn ($v) => …).
Where older books and tutorials are outdated
-
create_function()(Beak ch. 2, Kromann ch. 4) was deprecated in 7.2 and removed in 8.0; it compiled code from a string, likeeval(). -
Arrow functions (7.4), first-class callables (8.1), closures in constant expressions,
Closure::getCurrent()and the pipe operator (8.5), and partial application (8.6) are absent from books for PHP 7.3 and older. -
call_user_func()everywhere —$callable(...$args)works for every callable form. -
Closure::fromCallable()— superseded byfoo(...). -
The IIFE idiom
(function () { … })();still works and is sometimes used to create a private scope; in most cases a named function or a class method is clearer.