Arrow Functions
Arrow functions arrived in PHP 7.4 as a compact alternative to anonymous functions for the extremely common case of "take a value, transform it, return the result." Where a closure needs function, a parameter list, a body wrapped in braces, and often a use() clause, an arrow function collapses all of that into a single expression written with fn. The trade-off is that arrow functions can only ever contain one expression — there is no room for multiple statements, loops, or conditionals with bodies.
Basic syntax
fn(params) => expression
<?php $double = fn ($x) => $x * 2; echo $double(21);
42
There is no return keyword and no curly braces — the value of the single expression after => is automatically returned. Multiple parameters and default values work exactly as they do in a regular function signature.
Multiple parameters and a default value
<?php
$formatPrice = fn ($amount, $currency = 'CAD') => "{$amount} {$currency}";
echo $formatPrice(19.99);
echo "\n";
echo $formatPrice(45, 'USD');19.99 CAD 45 USD
Automatic capture of outer-scope variables
This is the headline feature that separates arrow functions from anonymous functions: an arrow function automatically captures, by value, any variable from the enclosing scope that it references. You never write a use() clause — PHP figures out which outer variables the expression touches and makes them available implicitly.
No use() clause needed
<?php $taxRate = 0.13; $withTax = array_map(fn ($price) => $price * (1 + $taxRate), [100, 200, 50]); print_r($withTax);
Array
(
[0] => 113
[1] => 226
[2] => 56.5
)Compare that with the equivalent anonymous function, which would raise an "undefined variable" warning unless you added use ($taxRate) explicitly. The arrow function version needs nothing extra — $taxRate is simply visible.
Closure vs. arrow function, side by side
The two snippets below do exactly the same thing. The closure spells out every step; the arrow function reduces it to the one expression that actually matters.
Same logic, two styles
<?php
$minStock = 5;
// As a closure — needs an explicit use() clause
$isLowStock = function ($item) use ($minStock) {
return $item['quantity'] < $minStock;
};
// As an arrow function — captures $minStock automatically
$isLowStockArrow = fn ($item) => $item['quantity'] < $minStock;
$item = ['name' => 'Widget', 'quantity' => 3];
var_dump($isLowStock($item));
var_dump($isLowStockArrow($item));bool(true) bool(true)
The single-expression limitation
This does NOT work
<?php
// Parse error: arrow functions cannot contain multiple statements
$process = fn ($x) => {
$y = $x * 2;
return $y + 1;
};If your logic needs more than one step — a temporary variable, a loop, several conditionals, logging, error handling — you need a full closure or a named function instead. The ternary operator and short-circuiting (&&, ||, ??) can sometimes squeeze surprisingly complex logic into a single expression, but resist the temptation to write unreadable one-liners just to stay inside fn.
When to prefer fn() vs. a full closure
Use an arrow function for short, single-expression transforms — the classic case being a callback passed to
array_map,array_filter, orusort.Use a full closure when the logic needs multiple statements: intermediate variables, loops, or several conditional branches.
Use a full closure when you need by-reference capture (
use (&$total)) — arrow functions only ever capture by value, automatically.Reach for a closure or named function once an arrow function starts nesting ternaries just to avoid a statement body — readability should win.
Chaining arrow functions with array_filter and array_map
<?php
$orders = [120, 45, 300, 15, 80];
$summary = array_map(
fn ($amount) => '$' . $amount . ' (with tax: $' . round($amount * 1.13, 2) . ')',
array_filter($orders, fn ($amount) => $amount >= 50)
);
print_r($summary);Array
(
[0] => $120 (with tax: $135.6)
[2] => $300 (with tax: $339)
[4] => $80 (with tax: $90.4)
)