Arrow functions (fn =>) — implicit capture, limitations
Concept
Arrow functions (fn) are PHP 7.4's concise closure syntax designed specifically for short expressions. The fn keyword creates a closure that implicitly captures all variables from the enclosing scope by value — no use clause needed. The body must be a single expression (not a block), and that expression's value is automatically returned. Arrow functions cannot contain return statements or multi-line blocks.
The implicit capture by value is both the key feature and the main limitation. Every variable visible in the outer scope at the point of arrow function definition is captured automatically. This is convenient for short callbacks but means you cannot capture by reference, and you cannot choose which variables to capture — all accessible variables are available. The captured values are frozen at definition time, just like explicit use ($x) in a regular closure.
Arrow functions can be nested: an inner fn implicitly captures from both its direct scope and any outer scopes, with each level captured by value.
| Feature | Regular closure (function) | Arrow function (fn) |
|---|---|---|
| Body | Block { ... } | Single expression |
| Capture | Explicit use ($x) | Implicit, all vars, by value |
| Reference capture | use (&$x) | Not supported |
| Multi-line | Yes | No |
| Explicit return | Required | Not allowed (implicit) |
$this in class | Auto-bound | Auto-bound |
Arrow functions shine in array_map, array_filter, usort, and collection pipelines. They are intentionally limited — if your callback needs reference capture, multiple statements, or selective capture, use a full closure.
Code Example
<?php
declare(strict_types=1);
$tax = 0.2;
// Arrow function implicitly captures $tax — no use() needed
$withTax = fn(float $price): float => $price * (1 + $tax);
echo $withTax(100.0) . PHP_EOL; // 120.0
$tax = 0.5; // change has no effect — captured at definition time
echo $withTax(100.0) . PHP_EOL; // still 120.0
// With array_filter and array_map — replacing verbose closures
$prices = [10.0, 25.0, 5.0, 100.0, 0.5];
$expensive = array_filter($prices, fn(float $p): bool => $p > 20.0);
$taxed = array_map(fn(float $p): float => $p * 1.2, $expensive);
var_dump(array_values($taxed));
// [30.0, 120.0]
// Nested arrow functions — each level captures from all outer scopes
$base = 10;
$multiplied = array_map(
fn(int $x): Closure => fn(int $y): int => ($x + $base) * $y,
[1, 2, 3]
);
echo $multiplied[0](5) . PHP_EOL; // (1 + 10) * 5 = 55
echo $multiplied[1](5) . PHP_EOL; // (2 + 10) * 5 = 60
// usort with arrow function — cleaner than Closure
$users = [
['name' => 'Charlie', 'age' => 30],
['name' => 'Alice', 'age' => 25],
['name' => 'Bob', 'age' => 35],
];
usort($users, fn(array $a, array $b): int => $a['age'] <=> $b['age']);
echo $users[0]['name'] . PHP_EOL; // Alice (youngest first)
// Cannot reference-capture — requires regular closure
$total = 0;
// $addFn = fn(int $n): void => $total += $n; // Fatal: by value, won't mutate $total
$addFn = function (int $n) use (&$total): void { $total += $n; }; // correct
array_walk([1, 2, 3], $addFn);
echo $total . PHP_EOL; // 6Interview Q&A
Q: What is the capture mechanism of arrow functions, and how does it differ from regular closures?
Arrow functions capture all variables from the enclosing lexical scope automatically by value at the time of definition. There is no use clause and no option to capture by reference. Regular closures capture nothing unless explicitly listed in a use clause — you choose exactly which variables enter the closure's environment and whether each is captured by value or reference. The arrow function's implicit all-by-value capture is intentional for the common case of short read-only callbacks, but it is a limitation when you need mutable state. Another practical difference: because arrow functions capture by value implicitly, they cannot be used as accumulators or stateful callbacks without wrapping — a pattern where regular closures with use (&$state) are the correct tool.
Q: Are arrow functions always faster than regular closures?
Arrow functions produce slightly less overhead at definition time because they do not allocate a use environment list — the engine resolves captures differently. However, for most application code, this difference is negligible. The real performance argument is readability-driven: fn($x) => $x * 2 is visually lighter, reducing cognitive load in pipeline-heavy code. Where arrow functions do have a measurable advantage is in hot paths that create many short closures (sorting large collections, mapping streams) because the simpler capture mechanism means fewer heap allocations. Profile before optimising — the difference between arrow functions and regular closures is rarely the bottleneck in a web application.
Q: Can arrow functions replace regular closures in all situations in a Laravel codebase?
No. Arrow functions cannot be used when you need: reference capture (accumulating a value, counter, stateful callback), multiple statements, explicit return with early exit, or throw statements inside the body. In Laravel specifically, closures passed to Collection::each() that accumulate into an external variable must use use (&$result) — an arrow function would silently read the outer variable without modifying it. Route closures that span multiple lines, middleware inline definitions, and service provider binding closures all require full function syntax. Arrow functions are best treated as syntactic sugar for pure, single-expression transformations — the kind that appear in map, filter, reduce, and usort calls.