If you've written routes in Laravel, you've already used closures without realizing it. That function () { ... } sitting inside Route::get() — that's a closure. But closures go far deeper than routes. They're one of PHP's most powerful and flexible features, used everywhere from collections to callbacks to event handling.
Start From the Beginning — What is a Function
Before understanding closures, lock in the basics.
A normal function in PHP has a name. You define it once, call it anywhere by that name:
<?php
function add(int $a, int $b): int { return $a + $b; }
echo add(3, 4);
Output:
7Simple. The function lives at the global level, has an identity (add), and can be called from anywhere in your code.
A Closure is a Function Without a Name
A closure is the same concept — a block of code that takes inputs and returns output — but it has no name. It's anonymous:
<?php
function (int $a, int $b): int { return $a + $b; };
But wait — if it has no name, how do you use it? You assign it to a variable:
<?php
$add = function (int $a, int $b): int { return $a + $b; };
echo $add(3, 4);
Output:
7
The closure lives inside $add. You call it like a function using that variable. The variable is just a container holding the anonymous function.
The Three Ways to Write Closures in PHP
PHP has three distinct syntaxes for closures, each with its own use case.
1. Classic Anonymous Function
The original closure syntax, available since PHP 5.3:
<?php
$greet = function (string $name): string { return "Hello, " . $name . "!"; };
echo $greet("Gagan");
Hello, Gagan!
2. Arrow Function — fn
Introduced in PHP 7.4. Cleaner, single-expression syntax:
<?php
$greet = fn(string $name): string => "Hello, " . $name . "!";
echo $greet("Gagan");
Same output. Arrow functions are perfect for short, single-line operations. They can't span multiple lines.
3. Static Closure
A closure that cannot access $this — used inside classes when you want an anonymous function that doesn't bind to the object:
<?php
$multiply = static function (int $a, int $b): int { return $a * $b; };
echo $multiply(4, 5);
// Output: 20
The Key Feature — Closing Over Variables
This is what makes a closure a closure — it can capture variables from its surrounding scope.
Normal named functions are isolated. They cannot see variables defined outside them:
<?php
$discount = 10;
function calculatePrice(int $price): int { return $price - $discount; }
This throws an error — $discount is not visible inside the named function.
A closure solves this with the use keyword:
<?php
$discount = 10;
$calculatePrice = function (int $price) use ($discount): int { return $price - $discount; };
echo $calculatePrice(100);
90
The closure "closed over" $discount from the outer scope — captured it, made it available inside. This is literally where the name "closure" comes from.
Capture by Value vs Capture by Reference
By default, use captures the value at the time the closure is defined — not when it's called:
<?php
$discount = 10;
$calculatePrice = function (int $price) use ($discount): int { return $price - $discount; };
$discount = 50;
echo $calculatePrice(100);
90
$discount was 10 when the closure was defined. Changing it later has no effect inside the closure.
To capture by reference — so changes reflect inside the closure — use &:
<?php
$discount = 10;
$calculatePrice = function (int $price) use (&$discount): int { return $price - $discount; };
$discount = 50;
echo $calculatePrice(100);
50
Now the closure sees the live value of $discount.
Arrow Functions Capture Automatically
Arrow functions (fn) don't need use at all — they automatically capture all variables from the outer scope by value:
<?php
$discount = 10;
$calculatePrice = fn(int $price): int => $price - $discount;
echo $calculatePrice(100);
90
No use needed. This is one of the main reasons arrow functions are preferred for short operations.
Closures as Arguments — The Real Power
The most common and powerful use of closures is passing them as arguments to other functions. This is called a callback.
With Array Functions
PHP's built-in array functions accept closures as callbacks:
<?php
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$evens = array_filter($numbers, fn(int $n): bool => $n % 2 === 0);
$doubled = array_map(fn(int $n): int => $n * 2, $numbers);
$sum = array_reduce($numbers, fn(int $carry, int $n): int => $carry + $n, 0);
print_r($evens); print_r($doubled); echo $sum;
[2, 4, 6, 8, 10]
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
55
Each closure runs once per array element. You define the logic inline — no need to create a named function just to use it once.
With usort
Sorting with custom logic:
<?php
$users = [ ['name' => 'Ravi', 'age' => 30], ['name' => 'Gagan', 'age' => 25], ['name' => 'Amit', 'age' => 28], ];
usort($users, fn(array $a, array $b): int => $a['age'] <=> $b['age']);
foreach ($users as $user) { echo $user['name'] . ' — ' . $user['age'] . PHP_EOL; }
Gagan — 25
Amit — 28
Ravi — 30
Closures in Laravel — Where You've Already Seen Them
In Routes
<?php
Route::get('/posts', function () { return "Showing all posts"; });
That function () { ... } is a closure passed as the second argument to Route::get(). Laravel stores it and calls it when someone visits /posts.
In Collections
Laravel's Collection class is built around closures:
<?php
$posts = collect([ ['title' => 'PHP Basics', 'published' => true], ['title' => 'Laravel Setup', 'published' => false], ['title' => 'Eloquent ORM', 'published' => true], ]);
$published = $posts ->filter(fn(array $post): bool => $post['published']) ->map(fn(array $post): string => strtoupper($post['title'])) ->values();
print_r($published->toArray());
['PHP BASICS', 'ELOQUENT ORM']
Every filter(), map(), reject(), sortBy(), each() call in Laravel Collections takes a closure. The entire fluent chaining system is built on closures being passed and executed per item.
In Middleware
<?php
Route::get('/dashboard', function () { return view('dashboard'); })->middleware(function ($request, $next) { if (!auth()->check()) { return redirect('/login'); } return $next($request); });
The middleware itself can be a closure — a function that receives the request, does something, and passes it forward.
In Event Listeners
<?php
Event::listen('user.registered', function (User $user): void { Mail::to($user->email)->send(new WelcomeMail($user)); });
Returning a Closure From a Function
Closures can also be returned from functions — this creates what's called a higher-order function:
<?php
function multiplier(int $factor): Closure { return fn(int $number): int => $number * $factor; }
$double = multiplier(2); $triple = multiplier(3); $tenX = multiplier(10);
echo $double(5); echo $triple(5); echo $tenX(5);
10
15
50
multiplier() returns a closure, not a value. Each call to multiplier() creates a new closure that has captured a different $factor. You now have three separate functions built from one factory function.
This pattern is used heavily in middleware pipelines, validation rule builders, and query scopes.
Closures Inside Classes — Binding $this
When you create a closure inside a class, it can access the object's properties via $this automatically:
<?php
class Cart { private float $taxRate = 0.18; private array $items = [];
public function addItem(string $name, float $price): void { $this->items[] = ['name' => $name, 'price' => $price]; }
public function getTotal(): float { $taxRate = $this->taxRate;
$total = array_reduce( $this->items, fn(float $carry, array $item): float => $carry + $item['price'], 0.0 );
return $total + ($total * $taxRate); } }
$cart = new Cart(); $cart->addItem('Laptop', 999.00); $cart->addItem('Mouse', 29.00);
echo $cart->getTotal();
1213.04Closure vs Named Function — When to Use Which
|
Situation |
Use |
|
Used once,
short logic |
Closure /
Arrow function |
|
Used in
multiple places |
Named
function |
|
Passed as
callback to array functions |
Arrow
function |
|
Complex
multi-line logic |
Named
function or method |
|
Route handler
in small apps |
Closure |
|
Route handler
in real applications |
Controller
method |
|
Laravel
Collection operations |
Arrow
function |
|
Event
listeners and hooks |
Closure |
The Complete Mental Model
Named Function → Has a name, global scope, called by name
Closure → No name, assigned to variable or passed directly
Arrow Function → Closure shorthand, auto-captures outer variables
Higher-Order Fn → A function that takes or returns another function
Every time you see function () { } or fn() => in PHP — that is a closure. Whether it's inside a route, a collection chain, an array function, or an event listener — same concept, same rules, different context.
The Bottom Line
A closure is simply a function that exists without a name. What makes it powerful is the combination of three things — it can be stored in a variable, passed as an argument to another function, and capture variables from the scope where it was defined. These three abilities together enable an entirely different style of programming in PHP — one that Laravel's entire collection system, routing layer, and event system is built on top of. Once closures click, a huge portion of Laravel's design suddenly becomes obvious.
No comments:
Post a Comment