Syntaxe callable de première classe

La syntaxe de callable de première classe est introduite à partir de PHP 8.1.0, comme une manière de créer des fonctions anonymes depuis des callable. Elle remplace la syntaxe des callables existante utilisant les chaînes et tableaux. L'avantage de cette syntaxe est qu'elle est accessible à l'analyse statique et utilise la portée du point où le callable est acquis.

La syntaxe CallableExpr(...) est utilisée pour créer un objet Closure depuis le callable. CallableExpr accepte toute expression qui peut être directement appelée dans la grammaire de PHP :

Exemple #1 Syntaxe callable de première classe basique

<?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(...); // invokable object
$f3 = $obj->method(...);
$f4 = $obj->$methodStr(...);
$f5 = Foo::staticmethod(...);
$f6 = $classStr::$staticmethodStr(...);
// traditional callable using string, array
$f7 = 'strlen'(...);
$f8 = [$obj, 'method'](...);
$f9 = [Foo::class, 'staticmethod'](...);
?>

Note:

Les ... font partie de la syntaxe et ne sont pas une omission.

CallableExpr(...) a les mêmes sémantiques que Closure::fromCallable(). C'est-à-dire, contrairement aux callables utilisant les chaînes et tableaux, CallableExpr(...) respecte la portée du point où il est créé :

Exemple #2 Comparaison de portée de CallableExpr(...) et des callables traditionnels

<?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
// This is because call is performed outside from Foo and visibility will be checked from this point.
class Foo1 {
public function
getPrivateMethod() {
// Uses the scope where the callable is acquired.
return $this->privateMethod(...); // identical to Closure::fromCallable([$this, 'privateMethod']);
}
private function
privateMethod() {
echo
__METHOD__, "\n";
}
}
$foo1 = new Foo1;
$privateMethod = $foo1->getPrivateMethod();
$privateMethod(); // Foo1::privateMethod
?>

Note:

La création d'objets avec cette syntaxe (e.g new Foo(...)) n'est pas supportée, car la syntaxe new Foo() n'est pas considérée comme un appel.

Note:

La syntaxe de callable de première classe ne peut pas être combinée avec l'opérateur nullsafe. Les deux cas suivants entraînent une erreur de compilation :

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

add a note

User Contributed Notes 2 notes

up
11
bienvenunet at yahoo dot com
10 months 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.
up
0
i dot am at sib dot ing
10 hours ago
Using first-class callable syntax is a good way to limit function access in WordPress CMS or other systems which use hook system.

<?php

class My_Hook_Class {
public function
__construct() {
add_filter( 'body_class', $this->filter_body_class( ... ), 100 ); // Pre 8.1 call syntax.
add_filter( 'body_class', array( $this, 'filter_body_class' ), 100 ); // 8.1+ call syntax.

}

private function
filter_body_class( array $classes ): array {
$classes[] = 'my-custom-class';

return
$classes;
}
}
?>

First call will add the body class, second will throw a TypeError

call_user_func_array(): Argument #1 ($callback) must be a valid callback, cannot access protected method My_Hook_Class::filter_body_class()
To Top