Skip to content
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