ConFoo 2025

Синтаксис callable-выражений как объектов первого класса

Синтаксис callable-выражений как объектов первого класса, которые разрешается передавать как аргумент, возвращать из функций или присваивать переменным, представили в PHP 8.1.0 как способ, который создаёт анонимные функции из callable-выражений. Новый синтаксис вытесняет предыдущий callable-синтаксис со строками и массивами. Преимущество нового синтаксиса состоит в доступности для статического анализа и в том, что новый синтаксис наследует область видимости переменных той точки, в которой callable-выражение получили.

Синтаксис CallableExpr(...) создаёт объект Closure из выражения, доступного для вызова, где CallableExpr — элемент синтаксиса, который принимает выражение, доступное для прямого вызова в терминах PHP-грамматики:

Пример #1 Простой пример синтаксиса callable-выражения как объекта первого класса

<?php

class Foo
{
public function
method() {}
public static function
staticmethod() {}
public function
__invoke() {}
}

$obj = new Foo();
$classStr = 'Foo';
$methodStr = 'method';
$staticmethodStr = 'staticmethod';

$f1 = strlen(...);
$f2 = $obj(...); // Вызов объекта как функции
$f3 = $obj->method(...);
$f4 = $obj->$methodStr(...);
$f5 = Foo::staticmethod(...);
$f6 = $classStr::$staticmethodStr(...);

// Традиционный синтаксис callable-выражений со строками и массивами
$f7 = 'strlen'(...);
$f8 = [$obj, 'method'](...);
$f9 = [Foo::class, 'staticmethod'](...);

?>

Замечание:

Оператор ... — часть синтаксиса, а не пропуск.

У выражения CallableExpr(...) та же семантика, что и у метода Closure::fromCallable(). То есть, в отличие от callable-синтаксиса со строками и массивами, синтаксис CallableExpr(...) учитывает область видимости в той точке, в которой его создали:

Пример #2 Сравнение области действия синтаксиса CallableExpr(...) и традиционного callable-синтаксиса

<?php

class Foo
{
public function
getPrivateMethod()
{
return [
$this, 'privateMethod'];
}

private function
privateMethod()
{
echo
__METHOD__, "\n";
}
}

$foo = new Foo();
$privateMethod = $foo->getPrivateMethod();
$privateMethod();
// Fatal error: Call to private method Foo::privateMethod() from global scope
// Причина фатальной ошибки в том, что вызов выполнили за пределами класса Foo,
// и с этого момента будет проверяться видимость.

class Foo1
{
public function
getPrivateMethod()
{
// Callable-выражение унаследует область видимости переменных,
// в которой получат выражение
return $this->privateMethod(...); // Значение возврата идентично значению вызова
// Closure::fromCallable([$this, 'privateMethod']);
}

private function
privateMethod()
{
echo
__METHOD__, "\n";
}
}

$foo1 = new Foo1();
$privateMethod = $foo1->getPrivateMethod();
$privateMethod(); // Foo1::privateMethod

?>

Замечание:

Синтаксисом наподобие new Foo(...) нельзя создать объект, поскольку PHP не считает синтаксис new Foo() вызовом.

Замечание:

Синтаксис, который создаёт объекты первого класса в виде callable-выражений, нельзя комбинировать с null-безопасным оператором. Каждая из следующих строк вызывает ошибку времени компиляции:

<?php

$obj
?->method(...);
$obj?->prop->method(...);

?>

Добавить

Примечания пользователей 1 note

up
15
bienvenunet at yahoo dot com
1 year ago
There's a major gotcha with this syntax that may not be apparent until you use this syntax and find you're getting "Cannot rebind scope of closure created from method" exceptions in some random library code.

As the documentation indicates, the first-class callable uses the scope at the point where the callable is acquired. This is fine as long as nothing in your code will attempt to bind the callable with the \Closure::bindTo method.

I found this the hard way by changing callables going to Laravel's Macroable functionality from the array style to the first-class callable style. The Macroable functionality \Closure::bindTo calls on the callable.

AFAIK, the only workaround is to use the uglier array syntax.
To Top