Anonymous Functions and Closures
Basic Anonymous Functions
<?php
// Anonymous function assigned to variable
$double = function (int $x): int {
return $x * 2;
};
echo $double(5); // 10
// Arrow functions (PHP 7.4+)
$triple = fn(int $x): int => $x * 3;
echo $triple(5); // 15
// Arrow functions with multiple statements
$formatPrice = fn(float $price): string =>
'\$' . number_format($price, 2);
echo $formatPrice(29.99); // '$29.99'
// Anonymous function in array_map
$prices = [10, 20, 30, 40, 50];
$doubled = array_map(fn($p) => $p * 2, $prices);
// [20, 40, 60, 80, 100]
// Anonymous function in array_filter
$expensive = array_filter($prices, fn($p) => $p > 25);
// [30, 40, 50]
Closure vs Anonymous Function
<?php
// Anonymous function (no external variable access)
$greet = function (string $name): string {
return "Hello, $name!";
};
// Closure (accesses external variable with 'use')
$greeting = 'Hello';
$greetClosure = function (string $name) use ($greeting): string {
return "$greeting, $name!";
};
echo $greetClosure('John'); // 'Hello, John!'
// Arrow functions automatically capture parent scope
$multiplier = 2;
$double = fn($x) => $x * $multiplier; // Automatically captures $multiplier
// Traditional closure needs explicit 'use'
$double = function ($x) use ($multiplier) {
return $x * $multiplier;
};
Variable Binding with use
<?php
// By value (copy) - default
$factor = 10;
$multiply = function ($x) use ($factor) {
return $x * $factor;
};
$factor = 20;
echo $multiply(5); // 50 (uses original 10, not 20)
// By reference (&)
$counter = 0;
$increment = function () use (&$counter) {
$counter++;
};
$increment();
$increment();
echo $counter; // 2 (modified by closure)
// Multiple variables
$name = 'John';
$age = 30;
$describe = function () use ($name, &$age) {
$age++;
return "$name is $age years old";
};
echo $describe(); // 'John is 31 years old'
Practical Examples
<?php
// Custom sorting
$products = [
['name' => 'Widget', 'price' => 29.99],
['name' => 'Gadget', 'price' => 19.99],
['name' => 'Doohickey', 'price' => 39.99]
];
usort($products, fn($a, $b) => $a['price'] <=> $b['price']);
// Sorted by price ascending
// Currying function
curriedAdd = function (int $a) {
return function (int $b) use ($a) {
return $a + $b;
};
};
$add5 = $curriedAdd(5);
echo $add5(3); // 8
echo $add5(10); // 15
// Memoization/caching
function memoize(callable $fn): callable
{
$cache = [];
return function ($key) use ($fn, &$cache) {
if (!isset($cache[$key])) {
$cache[$key] = $fn($key);
}
return $cache[$key];
};
}
$expensiveCalc = memoize(function ($n) {
// Expensive computation
return $n * $n;
});
echo $expensiveCalc(5); // Computes and caches
echo $expensiveCalc(5); // Returns cached value
Key Takeaway
Anonymous functions are functions without a name. Closures capture external variables with the use keyword. Arrow functions (fn) automatically capture parent scope. Use & for reference binding.
Closure::bind and Scope Manipulation
Closure::bind
<?php
class Product
{
private string $name;
private float $price;
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->price = $price;
}
}
$product = new Product('Widget', 29.99);
// Can't access private properties normally
// echo $product->name; // Error!
// Use Closure::bind to access private scope
$nameGetter = Closure::bind(
fn(Product $p) => $p->name,
null, // newThis (null = unbound)
Product::class // scope
);
echo $nameGetter($product); // 'Widget'
Modifying Private Properties
<?php
// Closure::bind can also modify private properties
$priceSetter = Closure::bind(
function (Product $p, float $newPrice) {
$p->price = $newPrice;
},
null,
Product::class
);
$priceSetter($product, 49.99);
// $product->price is now 49.99
Closure::fromCallable
<?php
// Convert a callable to a Closure
function add(int $a, int $b): int
{
return $a + $b;
}
$closure = Closure::fromCallable('add');
echo $closure(5, 3); // 8
// In PHP 8.0+, you can use First-class Callable Syntax
$closure = add(...);
echo $closure(5, 3); // 8
// Useful for passing methods as callbacks
$product = new Product('Widget', 29.99);
$nameGetter = Closure::fromCallable([$product, 'getName']);
// Or in PHP 8.0+: $nameGetter = $product->getName(...);
Binding with Object Scope
<?php
class Logger
{
private string $prefix;
public function __construct(string $prefix)
{
$this->prefix = $prefix;
}
public function createLogger(): Closure
{
// Bind closure to this object's scope
return Closure::bind(
function (string $message) {
return "[$this->prefix] $message";
},
$this, // Bind to this instance
$this // Scope to this class
);
}
}
$logger = new Logger('APP');
$log = $logger->createLogger();
echo $log('Hello'); // '[APP] Hello'
// Without binding, $this->prefix would fail
Closure in Magento Plugins
<?php
// Magento plugins often use closures
namespace Vendor\Module\Plugin;
class ProductPlugin
{
// Around plugin receives $proceed as a closure
public function aroundGetPrice(
\Magento\Catalog\Model\Product $subject,
callable $proceed, // This is a closure!
$qty = null
): float {
$start = microtime(true);
// Call original method via closure
$result = $proceed($qty);
$time = microtime(true) - $start;
error_log("getPrice took {$time}s");
return $result;
}
}
Key Takeaway
Closure::bind allows accessing and modifying private/protected properties by changing the closure's scope. It's used in testing, debugging, and Magento's plugin system. First-class callable syntax (fn(...)) makes this cleaner in PHP 8.
Callback Patterns and Magento Usage
Callback Patterns
<?php
// Pattern 1: Strategy pattern with closures
$pricingStrategies = [
'regular' => fn(float $price) => $price,
'wholesale' => fn(float $price) => $price * 0.7,
'vip' => fn(float $price) => $price * 0.85,
];
function calculatePrice(float $price, string $strategy): float
{
global $pricingStrategies;
return $pricingStrategies[$strategy]($price);
}
echo calculatePrice(100, 'wholesale'); // 70
// Pattern 2: Middleware pipeline
function pipeline(array $middlewares, callable $handler): callable
{
foreach (array_reverse($middlewares) as $middleware) {
$handler = function ($request) use ($middleware, $handler) {
return $middleware($request, $handler);
};
}
return $handler;
}
$middlewares = [
fn($req, $next) => $next(array_merge($req, ['auth' => true])),
fn($req, $next) => $next(array_merge($req, ['timestamp' => time()])),
];
$pipeline = pipeline($middlewares, fn($req) => $req);
$result = $pipeline(['data' => 'test']);
// ['data' => 'test', 'auth' => true, 'timestamp' => ...]
// Pattern 3: Event handling
$eventHandlers = [];
function on(string $event, callable $handler): void
{
global $eventHandlers;
$eventHandlers[$event][] = $handler;
}
function emit(string $event, array $data): void
{
global $eventHandlers;
foreach ($eventHandlers[$event] ?? [] as $handler) {
$handler($data);
}
}
on('product.save', fn($data) => echo "Saving: {$data['name']}\n");
on('product.save', fn($data) => error_log("Audit: {$data['name']}"));
emit('product.save', ['name' => 'Widget']);
Closures in Magento Collections
<?php
// Magento collection filtering with closures
$collection = $this->productCollectionFactory->create();
// Filter by closure
$collection->addFieldToFilter('price', ['gt' => 50]);
// Custom filter with callback
$collection->getSelect()->where(
function ($select) {
$select->where('price > ?', 50);
$select->where('status = ?', 1);
}
);
// Reduce collection to array
$prices = $collection->walk(function ($product) {
return $product->getPrice();
});
// Sort with closure
$products = $collection->getItems();
uasort($products, function ($a, $b) {
return $a->getPrice() <=> $b->getPrice();
});
Closures for Lazy Evaluation
<?php
// Lazy evaluation - defer expensive computation
class LazyValue
{
private ?\Closure $computer;
private mixed $value;
private bool $computed = false;
public function __construct(\Closure $computer)
{
$this->computer = $computer;
}
public function get(): mixed
{
if (!$this->computed) {
$this->value = ($this->computer)();
$this->computed = true;
$this->computer = null; // Free memory
}
return $this->value;
}
}
// Only computes when ->get() is called
$lazyResult = new LazyValue(function () {
echo "Computing...\n";
return expensiveDatabaseQuery();
});
// No computation yet
echo "Not computed yet\n";
// Computed on first access
$result = $lazyResult->get(); // 'Computing...'
$result = $lazyResult->get(); // No computation, cached
Closures for Dependency Injection
<?php
// Factory pattern with closures
$container = [
'productRepository' => function () use ($container) {
return new ProductRepository(
$container['db'](),
$container['cache']()
);
},
'db' => fn() => new PDO('mysql:host=localhost', 'root', ''),
'cache' => fn() => new RedisCache(),
];
// Lazy instantiation - only creates when called
$repo = $container['productRepository']();
Key Takeaway
Closures are essential for callbacks, event handling, and lazy evaluation in Magento. They enable strategy patterns, middleware pipelines, and dependency injection. Magento's plugin system relies on closures for method interception.
Quiz
1. What does the 'use' keyword do in a closure?
2. What is the difference between fn() and function() in PHP?
3. What does Closure::bind do?
4. How does Magento use closures in plugins?
5. What is lazy evaluation with closures?
Flashcards
Question
What is a closure in PHP?
Click to reveal answer
Answer
An anonymous function that can capture variables from its parent scope using the 'use' keyword. Created with function() or fn().
Question
What is the difference between fn() and function()?
Click to reveal answer
Answer
fn() automatically captures parent scope. function() requires explicit 'use' keyword. fn() is limited to a single expression.
Question
What does Closure::bind do?
Click to reveal answer
Answer
Changes the scope and $this binding of a closure. Allows accessing private/protected class members. Used for testing and debugging.
Question
How are closures used in Magento plugins?
Click to reveal answer
Answer
The $proceed parameter in around plugins is a closure. It calls the original method. You can wrap it with before/after logic.
Question
What is variable binding with & in use?
Click to reveal answer
Answer
Pass by reference: use (&$var). The closure can modify the original variable. Without &, it gets a copy.
Question
What is lazy evaluation?
Click to reveal answer
Answer
Deferring expensive computation in a closure until the value is actually needed. Result is cached for subsequent calls.
Question
What is Closure::fromCallable?
Click to reveal answer
Answer
Converts a callable (function name, method array) to a Closure object. In PHP 8.0+: use First-class Callable Syntax (fn(...)).
Question
What is currying with closures?
Click to reveal answer
Answer
Creating a function that returns another function. Example: $add5 = fn($x) => fn($y) => $x + $y; $add5(3) returns fn($y) => 3 + $y.
Revision Notes
Key Takeaways
- 1. Anonymous functions are functions without a name, assigned to variables
- 2. Closures use 'use' keyword to capture external variables
- 3. Arrow functions (fn) automatically capture parent scope
- 4. Closure::bind changes scope and $this binding for private access
- 5. Magento plugins use closures for method interception ($proceed)
- 6. Closures enable strategy patterns, middleware, and lazy evaluation
- 7. Use & in use statement for reference binding (modify original)
Interview Tips
- • Explain the difference between anonymous functions and closures
- • Describe when to use fn() vs function()
- • Know how Closure::bind works and when to use it
- • Explain how Magento plugins use closures
- • Give examples of callback patterns (strategy, middleware)
Cheat Sheet
PHP Closures Cheat Sheet
Basic Syntax:
$fn = function($x) { return $x * 2; };
$fn = fn($x) => $x * 2;
Variable Binding:
$factor = 10;
$fn = function($x) use ($factor) { return $x * $factor; };
$fn = function() use (&$counter) { $counter++; };
Arrow Functions:
$double = fn($x) => $x * 2;
$add = fn($a, $b) => $a + $b;
Closure::bind:
$getter = Closure::bind(fn($p) => $p->name, null, Product::class);
Array Functions:
array_map(fn($x) => $x * 2, $arr)
array_filter($arr, fn($x) => $x > 10)
array_reduce($arr, fn($c, $x) => $c + $x, 0)
usort($arr, fn($a, $b) => $a <=> $b)