advanced php language

Closures in Depth

A closure is an anonymous function that can be stored in a variable, passed as an argument, returned from another function, and optionally capture variables from the surrounding scope.

Closures are common in PHP collection operations, routing, middleware, event handlers, array functions, lazy callbacks, test setup, dependency factories, and small pieces of local behaviour.

Basic Closures

PHP example
<?php

declare(strict_types=1);

$formatName = function (string $firstName, string $lastName): string {
    return $firstName . ' ' . $lastName;
};

echo $formatName('Ada', 'Lovelace') . PHP_EOL;

// Prints:
// Ada Lovelace

The closure is stored in $formatName and called like a function.

Capturing Variables With use

Closures do not automatically capture local variables. Use use to bring variables into the closure.

PHP example
<?php

declare(strict_types=1);

$vatRate = 0.2;

$addVat = function (int $netPence) use ($vatRate): int {
    return $netPence + (int) round($netPence * $vatRate);
};

echo $addVat(1000) . PHP_EOL;

// Prints:
// 1200

By default, captured variables are captured by value. Changing $vatRate later does not change what this closure captured.

Capturing By Reference

You can capture by reference with &, but use it carefully.

PHP example
<?php

declare(strict_types=1);

$count = 0;

$increment = function () use (&$count): void {
    $count++;
};

$increment();
$increment();

echo $count . PHP_EOL;

// Prints:
// 2

Reference capture is useful for small local counters or accumulators, but it can make data flow harder to see. Prefer returning values when possible.

Arrow Functions

Arrow functions are shorter closures. They automatically capture variables by value and contain one expression.

PHP example
<?php

declare(strict_types=1);

$multiplier = 3;
$numbers = [1, 2, 3];

$scaled = array_map(
    fn (int $number): int => $number * $multiplier,
    $numbers,
);

echo implode(', ', $scaled) . PHP_EOL;

// Prints:
// 3, 6, 9

Arrow functions are good for short transformations. Use a normal closure or named function when the logic needs several statements.

Closures As Callbacks

Many PHP functions accept callbacks.

PHP example
<?php

declare(strict_types=1);

$orders = [
    ['id' => 1, 'status' => 'paid'],
    ['id' => 2, 'status' => 'draft'],
    ['id' => 3, 'status' => 'paid'],
];

$paidOrders = array_filter(
    $orders,
    fn (array $order): bool => $order['status'] === 'paid',
);

foreach ($paidOrders as $order) {
    echo $order['id'] . PHP_EOL;
}

// Prints:
// 1
// 3

Closures are useful when the behaviour is small and local to the operation.

Returning Closures

Functions can return closures. This is useful for building configured callbacks.

PHP example
<?php

declare(strict_types=1);

function minimumTotalFilter(int $minimumPence): Closure
{
    return fn (int $totalPence): bool => $totalPence >= $minimumPence;
}

$isLargeOrder = minimumTotalFilter(5000);

echo $isLargeOrder(7500) ? 'large' : 'small';
echo PHP_EOL;

// Prints:
// large

The returned closure remembers $minimumPence.

$this Inside Closures

Closures created inside object methods can use $this.

PHP example
<?php

declare(strict_types=1);

final class Prefixer
{
    public function __construct(
        private string $prefix,
    ) {
    }

    public function formatter(): Closure
    {
        return function (string $value): string {
            return $this->prefix . $value;
        };
    }
}

$formatter = new Prefixer('ID-')->formatter();

echo $formatter('42') . PHP_EOL;

// Prints:
// ID-42

This can be useful, but avoid hiding too much object behaviour inside anonymous functions.

When A Named Class Is Clearer

A closure is good for small local behaviour. A named class is better when the behaviour has a name, dependencies, tests, or reuse.

For example, a short filter in array_filter() is fine as a closure. A complex pricing rule with configuration and tests should probably be a DiscountCalculator, ShippingRule, or invokable class.

What You Should Be Able To Do

After this lesson, you should be able to write closures, capture values with use, understand by-value and by-reference capture, use arrow functions, pass closures as callbacks, and know when a named class is clearer.

For junior work, this matters because closures appear throughout modern PHP, especially in framework configuration, collections, tests, and callback-driven APIs.

Practice

Practice: Build Configured Order Filters

Create a small PHP example that returns configured closures.

Task

Build:

  • an array of order totals in pence
  • a minimumTotalFilter() function that returns a closure
  • an array_filter() call using that closure
  • an arrow function that formats the filtered totals for output

Use strict types. Keep the expected output in the PHP code block as printed lines or comments.

Check Your Work

Confirm:

  • the returned closure remembers the minimum total
  • only orders at or above the minimum remain
  • the arrow function captures any needed values by value

Afterward, explain when this should become a named class instead of a closure.

Show solution

This solution returns a closure that remembers the minimum total and uses an arrow function for a small formatting step.

PHP example
<?php

declare(strict_types=1);

function minimumTotalFilter(int $minimumPence): Closure
{
    return fn (int $totalPence): bool => $totalPence >= $minimumPence;
}

$orderTotals = [1200, 7500, 4999, 10000];

$largeOrders = array_filter(
    $orderTotals,
    minimumTotalFilter(5000),
);

$formatted = array_map(
    fn (int $totalPence): string => 'GBP ' . number_format($totalPence / 100, 2),
    $largeOrders,
);

foreach ($formatted as $total) {
    echo $total . PHP_EOL;
}

// Prints:
// GBP 75.00
// GBP 100.00

This should become a named class if the filter gains dependencies, several rules, its own tests, or a business name that matters outside this one local operation.