Table of Contents

PHP RFC: Support Closures in constant expressions

Introduction

Several PHP constructs are limited to accept “constant expressions” only. These expressions may only contain a limited number of operations that can roughly be summarized as “immutable values”. Notably attribute parameters are a construct that only accepts constant expressions and Closures are not currently part of the set of allowed operations in constant expressions.

As Closures are effectively just PHP source code (or rather: PHP Opcodes) they are an immutable value (when limiting some of the features) and as such there is no fundamental reason why they should not be allowed within constant expressions. And indeed there are some use cases that would be enabled by allowing Closures to appear in constant expressions.

As an example, it would enable a userland definition of array_filter() with the default filter callback that checks for emptiness, without needing to make the callback parameter nullable:

<?php
 
function my_array_filter(
    array $array,
    Closure $callback = static function ($item) { return !empty($item); },
) {
    $result = [];
 
    foreach ($array as $item) {
        if ($callback($item)) {
            $result[] = $item;
        }
    }
 
    return $result;
}
 
var_dump(my_array_filter([
    0, 1, 2,
    '', 'foo', 'bar',
]));
 
?>

Proposal

This RFC proposes that it shall be legal to include Closures in constant expressions. This includes:

Constraints

If Closures are placed in constant expressions they are subject to the following constraints:

Scoping

As with other constant-expressions, Closures defined in constant expressions follow the expected scoping rules of the context they are placed in. This means that Closures in property default values may access private properties, methods, and class constants of the class where they are defined, similarly to how a Closure defined in the constructor and stored in a property may access those private members. Likewise are Closures in attribute parameters allowed to access private members of the surrounding class.

Closures in sub-expressions

Closures behave like any other operation within a constant expression, thus they may be part of a sub-expression of another operation. While it is not particularly useful to use Closures as an operand to mathematical expressions, it will also not break anything.

However the following operations are examples of how Closures can usefully be included in sub-expressions.

Defining a list of Closures in a default parameter:

<?php
 
function foo(
    string $input,
    array $callbacks = [
        static function ($value) {
            return \strtoupper($value);
        },
        static function ($value) {
            return \preg_replace('/[^A-Z]/', '', $value);
        },
    ]
) {
    foreach ($callbacks as $callback) {
        $input = $callback($input);
    }
 
    return $input;
}
 
var_dump(foo('Hello, World!')); // string(10) "HELLOWORLD"

Passing a Closure to a new expression:

<?php
 
class MyObject
{
    public function __construct(private Closure $callback) {}
}
 
const Foo = new MyObject(static function () {
    return 'foo';
});

Use Cases

Custom field validation for an attribute-based object validation library:

final class Locale
{
    #[Validator\Custom(static function (string $languageCode): bool {
        return \preg_match('/^[a-z][a-z]$/', $languageCode);
    })]
    public string $languageCode;
}

Testcase generation for a testing library:

final class CalculatorTest
{
    #[Test\CaseGenerator(static function (): iterable {
        for ($i = -10; $i <= 10; $i++) {
            yield [$i, $i, 0];
            yield [$i, 0, $i];
            yield [0, $i, ($i * -1)];
        }
    })]
    public function testSubtraction(int $minuend, float $subtrahend, int $result)
    {
        \assert(Calculator::subtract($minuend, $subtrahend) === $result);
    }
}

Custom formatting for an attribute-based serializer:

final class LogEntry
{
     public string $message;
 
     #[Serialize\Custom(static function (string $severity): string {
         return \strtoupper($severity);
     })]
     public string $severity;
}

Backward Incompatible Changes

None. Placing Closures into constant-expressions previously resulted in a compile-time error.

Nevertheless, as with every RFC that changes what previously was a compile-time error to be valid PHP code, this RFC requires changes to static analyzers and IDEs to correctly understand the semantics of the code and not erroneously report errors.

Proposed PHP Version(s)

Next PHP 8.x (8.5).

RFC Impact

To SAPIs

None.

To Existing Extensions

None.

To Opcache

Opcache needs to be adjusted to correctly store Closures in constant expressions in SHM. The PoC PR includes the necessary Opcache changes and passes all tests with Opcache / JIT enabled.

New Constants

None.

php.ini Defaults

None.

Open Issues

n/a

Unaffected PHP Functionality

Only constant expression are affected by the change and only in a way that Closure objects may appear in places where they previously could not appear in (e.g. class constants).

Future Scope

Proposed Voting Choices

Support Closures in constant expressions as proposed?
Real name Yes No
Final result: 0 0
This poll has been closed.

Patches and Tests

https://github.com/php/php-src/pull/16458

Implementation

n/a

References

Rejected Features

n/a