Use First-class Callable Syntax in Constant Expressions in PHP 8.5

Use First-class Callable Syntax in Constant Expressions in PHP 8.5

First-class callable syntax in PHP (introduced in PHP 8.1) allows us to create an anonymous function (Closure) using the expression CallableExpr(...). It is safer and more refactor-friendly than using string or array callables. Since PHP 8.5, first-class callable syntax is allowed in constant expressions. This includes default values of parameters and properties, constants, and attribute parameters.

Assume we have a function that accepts an optional callback for filtering array elements. Before PHP 8.5, we had to default the closure to null and assign a fallback closure when none was provided by the user.

<?php

function arrayFilter(array $data, ?Closure $callback = null): array
{
    $callback ??= is_int(...);
    $result = [];
    foreach ($data as $item) {
        if ($callback($item)) {
            $result[] = $item;
        }
    }

    return $result;
}

$data = ['', 1, 0, false, '1'];
var_dump(arrayFilter($data)); // array(2) {[0]=>int(1) [1]=>int(0)}

Since PHP 8.5, first-class callable syntax can be used directly as a default parameter value, eliminating the need to set the closure to null and assign a fallback.

<?php

function arrayFilter(array $data, Closure $callback = is_int(...)): array
{
    $result = [];
    foreach ($data as $item) {
        if ($callback($item)) {
            $result[] = $item;
        }
    }

    return $result;
}

$data = ['', 1, 0, false, '1'];
var_dump(arrayFilter($data)); // array(2) {[0]=>int(1) [1]=>int(0)}

Other cases where first-class callable syntax is permitted in constant expressions include:

  • Default values of properties

ArrayFilter.php

<?php

class ArrayFilter
{
    private Closure $callback = is_int(...);

    public function setCallback(Closure $callback): void
    {
        $this->callback = $callback;
    }

    public function run(array $data): array
    {
        $result = [];
        foreach ($data as $item) {
            if (($this->callback)($item)) {
                $result[] = $item;
            }
        }

        return $result;
    }
}
<?php

require_once __DIR__.'/ArrayFilter.php';

$arrayFilter = new ArrayFilter();

$data = ['', 1, 0, false, '1'];
var_dump($arrayFilter->run($data)); // array(2) {[0]=>int(1) [1]=>int(0)}
  • Constants

FilterConst.php

<?php

class FilterConst
{
    public const Closure DEFAULT = is_int(...);
}
<?php

require_once __DIR__.'/FilterConst.php';

$data = ['', 1, 0, false, '1'];
var_dump(array_filter($data, FilterConst::DEFAULT)); // array(2) {[0]=>int(1) [1]=>int(0)}
  • Attribute parameters

Transform.php

<?php

#[Attribute(Attribute::TARGET_PROPERTY)]
class Transform
{
    public function __construct(public Closure $callback)
    {
    }
}

User.php

<?php

class User
{
    public function __construct(#[Transform(ucfirst(...))] private string $name)
    {
        $this->name = $this->applyTransform('name', $name);
    }

    public function getName(): string
    {
        return $this->name;
    }

    private function applyTransform(string $property, mixed $value): mixed
    {
        $reflection = new ReflectionProperty($this, $property);
        $attribute = $reflection->getAttributes(Transform::class)[0]->newInstance();

        return ($attribute->callback)($value);
    }
}
<?php

require_once __DIR__.'/Transform.php';
require_once __DIR__.'/User.php';

$user = new User('john');
echo $user->getName(); // John

Leave a Comment

Cancel reply

Your email address will not be published.