Skip to content

Support first-class callables in const-expressions #17213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 20, 2025
4 changes: 2 additions & 2 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ PHP NEWS
. Fixed bug GH-16665 (\array and \callable should not be usable in
class_alias). (nielsdos)
. Added PHP_BUILD_DATE constant. (cmb)
. Added support for Closures in constant expressions. (timwolla,
Volker Dusch)
. Added support for Closures and first class callables in constant
expressions. (timwolla, Volker Dusch)
. Use `clock_gettime_nsec_np()` for high resolution timer on macOS
if available. (timwolla)
. Implement GH-15680 (Enhance zend_dump_op_array to properly represent
Expand Down
4 changes: 3 additions & 1 deletion UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ PHP 8.5 UPGRADE NOTES

- Core:
. Closure is now a proper subtype of callable
. Added support for Closures in constant expressions.
. Added support for Closures and first class callables in constant
expressions.
RFC: https://wiki.php.net/rfc/closures_in_const_expr
RFC: https://wiki.php.net/rfc/fcc_in_const_expr
. Fatal Errors (such as an exceeded maximum execution time) now include a
backtrace.
RFC: https://wiki.php.net/rfc/error_backtraces_v2
Expand Down
50 changes: 50 additions & 0 deletions Zend/tests/first_class_callable/constexpr/attributes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Allow defining FCC in attributes
--EXTENSIONS--
reflection
--FILE--
<?php

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Attr {
public function __construct(public Closure $value) {
var_dump($value('abc'));
}
}

#[Attr(strrev(...))]
#[Attr(strlen(...))]
class C {}

foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
var_dump($reflectionAttribute->newInstance());
}

?>
--EXPECTF--
string(3) "cba"
object(Attr)#%d (1) {
["value"]=>
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
}
int(3)
object(Attr)#%d (1) {
["value"]=>
object(Closure)#%d (2) {
["function"]=>
string(6) "strlen"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
AST printing for FCC in attributes
--FILE--
<?php

// Do not use `false &&` to fully evaluate the function / class definition.

try {
\assert(
!
#[Attr(strrev(...))]
function () { }
);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

try {
\assert(
!
new #[Attr(strrev(...))]
class {}
);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
assert(!#[Attr(strrev(...))] function () {
})
assert(!new #[Attr(strrev(...))] class {
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
AST printing for FCC in attributes at runtime
--FILE--
<?php

namespace Test;

class Clazz {
#[Attr(strrev(...), \strrev(...), Clazz::foo(...), self::foo(...))]
function foo() { }
}

$r = new \ReflectionMethod(Clazz::class, 'foo');
foreach ($r->getAttributes() as $attribute) {
echo $attribute;
}

?>
--EXPECT--
Attribute [ Test\Attr ] {
- Arguments [4] {
Argument #0 [ Test\strrev(...) ]
Argument #1 [ \strrev(...) ]
Argument #2 [ \Test\Clazz::foo(...) ]
Argument #3 [ self::foo(...) ]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
FCC in attribute may access private methods
--EXTENSIONS--
reflection
--FILE--
<?php

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Attr {
public function __construct(public Closure $value) {}
}

#[Attr(C::myMethod(...))]
class C {
private static function myMethod(string $foo) {
echo "Called ", __METHOD__, PHP_EOL;
var_dump($foo);
}
}

foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
($reflectionAttribute->newInstance()->value)('abc');
}

?>
--EXPECT--
Called C::myMethod
string(3) "abc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
FCC in attribute may not access unrelated private methods
--EXTENSIONS--
reflection
--FILE--
<?php

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Attr {
public function __construct(public Closure $value) {}
}

class E {
private static function myMethod(string $foo) {
echo "Called ", __METHOD__, PHP_EOL;
var_dump($foo);
}
}

#[Attr(E::myMethod(...))]
class C {
}

foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
($reflectionAttribute->newInstance()->value)('abc');
}

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to private method E::myMethod() from scope C in %s:%d
Stack trace:
#0 %s(%d): ReflectionAttribute->newInstance()
#1 {main}
thrown in %s on line %d
31 changes: 31 additions & 0 deletions Zend/tests/first_class_callable/constexpr/autoload.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
FCC in const expression triggers autoloader.
--FILE--
<?php

spl_autoload_register(static function ($class) {
echo "Autoloading {$class}", PHP_EOL;
eval(
<<<'EOT'
class AutoloadedClass {
public static function withStaticMethod() {
echo "Called ", __METHOD__, PHP_EOL;
}
}
EOT
);
});

const Closure = AutoloadedClass::withStaticMethod(...);

var_dump(Closure);
(Closure)();

?>
--EXPECTF--
Autoloading AutoloadedClass
object(Closure)#%d (1) {
["function"]=>
string(16) "withStaticMethod"
}
Called AutoloadedClass::withStaticMethod
22 changes: 22 additions & 0 deletions Zend/tests/first_class_callable/constexpr/basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Allow defining FCC in const expressions.
--FILE--
<?php

const Closure = strrev(...);

var_dump(Closure);
var_dump((Closure)("abc"));

?>
--EXPECTF--
object(Closure)#%d (2) {
["function"]=>
string(%d) "%s"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
string(3) "cba"
22 changes: 22 additions & 0 deletions Zend/tests/first_class_callable/constexpr/case_insensitive.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Allow defining FCC in const expressions with case-insensitive names.
--FILE--
<?php

const Closure = StrRev(...);

var_dump(Closure);
var_dump((Closure)("abc"));

?>
--EXPECTF--
object(Closure)#%d (2) {
["function"]=>
string(%d) "%s"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
string(3) "cba"
24 changes: 24 additions & 0 deletions Zend/tests/first_class_callable/constexpr/class_const.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Allow defining FCC in class constants.
--FILE--
<?php

class C {
const Closure = strrev(...);
}

var_dump(C::Closure);
var_dump((C::Closure)("abc"));

?>
--EXPECTF--
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
string(3) "cba"
39 changes: 39 additions & 0 deletions Zend/tests/first_class_callable/constexpr/complex_array.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Allow defining FCC wrapped in an array in const expressions.
--FILE--
<?php

const Closure = [strrev(...), strlen(...)];

var_dump(Closure);

foreach (Closure as $closure) {
var_dump($closure("abc"));
}

?>
--EXPECTF--
array(2) {
[0]=>
object(Closure)#%d (2) {
["function"]=>
string(6) "strrev"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
[1]=>
object(Closure)#%d (2) {
["function"]=>
string(6) "strlen"
["parameter"]=>
array(1) {
["$string"]=>
string(10) "<required>"
}
}
}
string(3) "cba"
int(3)
18 changes: 18 additions & 0 deletions Zend/tests/first_class_callable/constexpr/default_args.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
FCC in default argument
--FILE--
<?php

function test(
Closure $name = strrev(...)
) {
var_dump($name("abc"));
}

test();
test(strlen(...));

?>
--EXPECT--
string(3) "cba"
int(3)
20 changes: 20 additions & 0 deletions Zend/tests/first_class_callable/constexpr/error_abstract.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
FCC in initializer errors for FCC on abstract method
--FILE--
<?php

abstract class Foo {
abstract public static function myMethod(string $foo);
}

const Closure = Foo::myMethod(...);

var_dump(Closure);
(Closure)("abc");

?>
--EXPECTF--
Fatal error: Uncaught Error: Cannot call abstract method Foo::myMethod() in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/first_class_callable/constexpr/error_dynamic_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
FCC in initializer errors for FCC on variable.
--FILE--
<?php

const Closure = $foo(...);

var_dump(Closure);

?>
--EXPECTF--
Fatal error: Cannot use dynamic function name in constant expression in %s on line %d
Loading
Loading