Closures & Arrow Functions
Reviewed & published by Brayan K
Functions in PHP are values — you can store them in variables, pass them as arguments, and return them from other functions. Master closures and the concise fn() => arrow syntax, and the whole world of array_map, array_filter and callbacks opens up.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Write anonymous functions and store them in variables
- Pass functions as arguments using the callable type
- Capture outer variables with use() — by value and by reference
- Write concise one-liners with fn() => arrow functions
- Drive array_map, array_filter and array_reduce with closures
- Build a factory that returns stateful closures
1️⃣ Anonymous Functions as Values
An anonymous function (also called a closure) is a function with no name. You assign it to a variable and call it through that variable with (). Because it's just a value, you can also pass it into another function — any parameter typed callable will accept it.
<?php
// A CLOSURE is a function with no name that you store in a variable
// (or pass into another function). PHP calls these "anonymous functions".
$greet = function (string $name): string {
return "Hello, $name!";
};
// Call it through the variable, with ().
echo $greet('Ada') . "\n"; // Hello, Ada!
// Because it's just a value, you can pass it as an argument.
function runTwice(callable $fn, string $arg): void {
echo $fn($arg) . "\n";
echo $fn($arg) . "\n";
}
runTwice($greet, 'Sam'); // prints the greeting twice
?>2️⃣ Capturing Scope with use()
Here's the part that surprises newcomers: a PHP closure does not automatically see variables from the code around it. You import the ones you need with use(...). By default use($x) captures a copy of the value at the moment the closure is defined. Add an ampersand — use(&$x) — to capture by reference so the closure and the outer code share one live variable.
<?php
// A closure does NOT see the outer scope automatically.
// You "capture" outer variables with the use() keyword.
$tax = 0.20;
// use ($tax) copies the value INTO the closure (by value).
$withTax = function (float $price) use ($tax): float {
return $price + ($price * $tax);
};
echo $withTax(100) . "\n"; // 120
// Changing $tax afterwards does NOT affect the closure — it captured a copy.
$tax = 0.50;
echo $withTax(100) . "\n"; // still 120
// To capture by REFERENCE (live link), add &: use (&$counter)
$counter = 0;
$tick = function () use (&$counter) { $counter++; };
$tick(); $tick(); $tick();
echo $counter . "\n"; // 3
?>3️⃣ Arrow Functions
Arrow functions (PHP 7.4+) are a compact closure for a single expression: fn($x) => $x * 2. They have two superpowers over the long form: there's an implicit return, and they auto-capture outer variables by value — no use() needed. The trade-off is they're limited to one expression.
<?php
// ARROW FUNCTIONS (PHP 7.4+) are a short closure for one expression.
// fn($x) => expression
// They AUTO-capture outer variables by value — no use() needed.
$multiplier = 3;
// Equivalent to: function ($n) use ($multiplier) { return $n * $multiplier; }
$triple = fn($n) => $n * $multiplier;
echo $triple(5) . "\n"; // 15
echo $triple(10) . "\n"; // 30
// Arrow functions shine inside array helpers.
$nums = [1, 2, 3, 4];
$doubled = array_map(fn($n) => $n * 2, $nums);
echo implode(', ', $doubled) . "\n"; // 2, 4, 6, 8
?>4️⃣ The Array-Function Trio
Closures really earn their keep with PHP's higher-order array functions. array_filter keeps items where your closure returns true, array_map transforms each item, and array_reduce folds a list down to a single value. Arrow functions make these read almost like English.
<?php
// Closures are the engine behind PHP's array helpers.
$products = [
['name' => 'Pen', 'price' => 2],
['name' => 'Mug', 'price' => 8],
['name' => 'Laptop', 'price' => 950],
];
// array_filter: keep only items whose closure returns true.
$affordable = array_filter($products, fn($p) => $p['price'] < 100);
// array_map: transform each item.
$names = array_map(fn($p) => $p['name'], $affordable);
// array_reduce: fold the list into a single value.
$total = array_reduce($products, fn($carry, $p) => $carry + $p['price'], 0);
echo 'Affordable: ' . implode(', ', $names) . "\n";
echo 'Total spend: $' . $total . "\n";
?>Now you try — fill in each ___ using the 👉 hint, then run it and check against the Output panel.
<?php
// 🎯 YOUR TURN — fill in each blank marked ___ , then run it.
$bonus = 10;
// 1) An arrow function that adds $bonus to its argument:
$addBonus = ___ ($score) => $score + $bonus; // 👉 the arrow-function keyword
echo $addBonus(90) . "\n"; // should print: 100
// 2) Double every number with array_map and an arrow function:
$nums = [3, 6, 9];
$doubled = array_map(___ ($n) => $n * 2, $nums); // 👉 same keyword again
echo implode(',', $doubled) . "\n"; // should print: 6,12,18
// ✅ Expected output:
// 100
// 6,12,18
?>🧩 Reorder Challenge
These lines build a by-reference counter closure — but they're scrambled. Put them in the order that prints 2.
B → D → A → E → C
Initialise $n (B), define the closure that captures it by reference (D), call it twice (A, E) so $n becomes 2, then echo it (C). The closure must exist before you call it.
🧠 Quick Recall — Predict the Output
6 — the arrow function captured $x by value (5) when it was defined. Reassigning $x to 100 afterwards has no effect.
2. What does this print?
1,4,9 — each element is squared by the arrow function.
3. What is the final value of $total?
7 — capturing by reference (&$total) lets the closure mutate the outer variable across both calls.
Common Errors (and the fix)
- "Undefined variable" inside a full closure — you forgot to import it. Add it to use(...), or switch to an arrow function which auto-captures.
- The closure ignores changes you made to a captured variable — use() copies by value. Use use(&$x) if you need a live link.
- "syntax error" after fn(...) with a brace — arrow functions take a single expression, not a { ... } block. Use a full closure for multiple statements.
- "Value of type Closure is not callable"-style errors — you tried to call the variable without (), or passed it where a non-callable was expected. Invoke with $fn(...).
Pro Tips
- 💡 Reach for fn() => for callbacks. Inside array_map/array_filter it's shorter and auto-captures the scope.
- 💡 Use a full closure when you need statements. Multiple lines, loops, or early returns won't fit in an arrow function's single expression.
- 💡 Be deliberate with &. By-reference capture is powerful for counters but easy to misuse — only add the ampersand when you truly want shared state.
📋 Quick Reference — Closures
| Syntax | Example | What It Does |
|---|---|---|
| function () {} | $f = function () { return 1; }; | Anonymous function (closure) |
| use ($x) | function () use ($x) {} | Capture by value |
| use (&$x) | function () use (&$x) {} | Capture by reference |
| fn() => | fn($n) => $n * 2 | Arrow function (auto-capture) |
| array_map | array_map($fn, $arr) | Transform each item |
| array_filter | array_filter($arr, $fn) | Keep items where true |
| array_reduce | array_reduce($arr, $fn, 0) | Fold to one value |
Mini-Challenge: Counter Factory
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments.
<?php
// 🎯 MINI-CHALLENGE: a counter factory.
// No code is filled in — work from the steps below, then run it.
//
// 1. Write a function makeCounter() that returns a CLOSURE.
// Inside makeCounter, start $count = 0.
// The returned closure must use (&$count), increment it, and return it.
// 2. Create $a = makeCounter(); and $b = makeCounter();
// 3. Call $a() three times and $b() once, echoing each result.
// Each counter must keep its OWN private count.
//
// ✅ Expected output:
// 1
// 2
// 3
// 1
// your code here
?>🎉 Lesson Complete!
- ✅ Closures are functions stored in variables — pass them anywhere a callable is accepted
- ✅ A full closure imports outer variables with use() — by value, or & for reference
- ✅ Arrow functions fn() => auto-capture and return one expression
- ✅ array_map, array_filter and array_reduce are powered by closures
Practice quiz
How does a classic closure (function (...) { ... }) access a variable from the surrounding scope?
- Automatically, like JavaScript
- Only with the global keyword
- You must import it with use()
- It cannot access outer variables
Answer: You must import it with use(). A PHP closure does not inherit outer scope automatically. You explicitly import variables with use($var).
What prints? $x = 5; $f = fn($n) => $n + $x; $x = 100; echo $f(1);
- 6
- 101
- 1
- 105
Answer: 6. The arrow function auto-captured $x by value (5) when defined. Reassigning $x to 100 afterwards has no effect, so 1 + 5 = 6.
What does use(&$x) with the ampersand do?
- Captures a copy of $x
- Makes $x read-only
- Renames $x inside the closure
- Captures $x by reference, sharing the live variable
Answer: Captures $x by reference, sharing the live variable. use(&$x) captures by reference: the closure shares the same variable as the outer scope and can read and modify the live value.
How do arrow functions (fn() =>) capture outer variables?
- They require use()
- They auto-capture by value any variable they reference
- They auto-capture by reference
- They cannot capture anything
Answer: They auto-capture by value any variable they reference. Arrow functions automatically capture by value any outer variable they mention — no use() needed.
What is the body limitation of an arrow function?
- It is limited to a single expression
- It can have at most 3 statements
- It cannot take parameters
- It cannot return a value
Answer: It is limited to a single expression. An arrow function (fn(...) => expression) is limited to one expression, with an implicit return. Use a full closure for multiple statements.
What does echo implode(',', array_map(fn($n) => $n * $n, [1, 2, 3])); print?
- 1,2,3
- 2,4,6
- 1,4,9
- 1,8,27
Answer: 1,4,9. array_map squares each element with the arrow function: 1, 4, 9.
With $tax = 0.2; $f = function ($p) use ($tax) { return $p + $p * $tax; }; then $tax = 0.5; what is $f(100)?
- 150
- 120
- 100
- 105
Answer: 120. use($tax) captured a copy of 0.2 at definition time. Changing $tax to 0.5 afterwards does not affect the closure, so 100 + 20 = 120.
Which array helper folds a list down to a single value?
- array_map
- array_filter
- array_merge
- array_reduce
Answer: array_reduce. array_reduce folds a list into one value (e.g. a running total); array_map transforms and array_filter keeps items.
What does the callable type hint mean?
- Only a closure is accepted
- Something you can invoke with (), including closures, function-name strings, and [obj, 'method']
- An array of functions
- A class that is instantiable
Answer: Something you can invoke with (), including closures, function-name strings, and [obj, 'method']. callable means 'something you can call like a function' — a closure, a function name string, an [object, 'method'] array, or an invokable object.
After $total = 0; $add = function ($n) use (&$total) { $total += $n; }; $add(3); $add(4); what is $total?
- 0
- 3
- 7
- 4
Answer: 7. Capturing by reference (&$total) lets the closure mutate the outer variable across both calls: 3 + 4 = 7.
Continue this course
- Previous: The match Expression
- Next: Regular Expressions (preg) — Match, capture, and replace text patterns with preg_match and preg_replace
- Quick reference: PHP cheat sheet
Frequently asked questions
What's the difference between a closure and an arrow function in PHP?
Both are anonymous functions you can store in a variable or pass around. A classic closure (function (...) { ... }) can hold many statements but must explicitly list which outer variables it captures with use(). An arrow function (fn(...) => expression) is limited to a single expression but auto-captures any outer variables it references, by value, with no use() needed. Use an arrow function for short one-liners like callbacks to array_map; use a full closure when you need multiple statements or want to capture by reference.
Why doesn't my closure see a variable from the surrounding code?
PHP closures do not inherit the outer scope automatically — unlike JavaScript. A full closure only sees the variables you explicitly import with use($var). If you forget use(), the variable is simply undefined inside the closure. Arrow functions (fn() =>) are the exception: they automatically capture by value any outer variable they mention, which is exactly why they feel more convenient for quick callbacks.
What does use(&$var) with the ampersand do?
By default use($var) captures a copy of the variable's value at the moment the closure is created, so later changes outside don't affect it. Adding the ampersand, use(&$var), captures by reference: the closure shares the same variable as the outer scope, so it can read and modify the live value, and outside changes are visible inside. This is how you build counters or accumulators that persist state across calls. Use it deliberately — accidental by-reference capture is a common source of confusing bugs.
What is the 'callable' type and how does it relate to closures?
callable is a type hint that means 'something you can call like a function'. A closure or arrow function satisfies callable, but so do a plain function name as a string ('strtoupper'), an array of [object, 'method'], or an invokable object. When you write function run(callable $fn), you're saying 'give me anything I can invoke with ()'. Closures are the most common callable, which is why the two terms often appear together.