Skip to content
intermediate Phase 6 · PHP Advanced

PHP Closures: Anonymous Functions and Callback Patterns

Master anonymous functions, closures, the use keyword, Closure::bind, and callback patterns used in Magento plugins.

45m
0 problems
Topic Progress 0%

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?

Question 1 options

2. What is the difference between fn() and function() in PHP?

Question 2 options

3. What does Closure::bind do?

Question 3 options

4. How does Magento use closures in plugins?

Question 4 options

5. What is lazy evaluation with closures?

Question 5 options

Flashcards

Question

What is a closure in PHP?

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()?

Answer

fn() automatically captures parent scope. function() requires explicit 'use' keyword. fn() is limited to a single expression.

Question

What does Closure::bind do?

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?

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?

Answer

Pass by reference: use (&$var). The closure can modify the original variable. Without &, it gets a copy.

Question

What is lazy evaluation?

Answer

Deferring expensive computation in a closure until the value is actually needed. Result is cached for subsequent calls.

Question

What is Closure::fromCallable?

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?

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)