Skip to content

Commit 2042fd3

Browse files
TimWollaedorian
andauthored
Support first-class callables in const-expressions (#17213)
RFC: https://wiki.php.net/rfc/fcc_in_const_expr Co-authored-by: Volker Dusch <[email protected]>
1 parent 252dd1e commit 2042fd3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1231
-15
lines changed

NEWS

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ PHP NEWS
1414
. Fixed bug GH-16665 (\array and \callable should not be usable in
1515
class_alias). (nielsdos)
1616
. Added PHP_BUILD_DATE constant. (cmb)
17-
. Added support for Closures in constant expressions. (timwolla,
18-
Volker Dusch)
17+
. Added support for Closures and first class callables in constant
18+
expressions. (timwolla, Volker Dusch)
1919
. Use `clock_gettime_nsec_np()` for high resolution timer on macOS
2020
if available. (timwolla)
2121
. Implement GH-15680 (Enhance zend_dump_op_array to properly represent

UPGRADING

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ PHP 8.5 UPGRADE NOTES
8585

8686
- Core:
8787
. Closure is now a proper subtype of callable
88-
. Added support for Closures in constant expressions.
88+
. Added support for Closures and first class callables in constant
89+
expressions.
8990
RFC: https://wiki.php.net/rfc/closures_in_const_expr
91+
RFC: https://wiki.php.net/rfc/fcc_in_const_expr
9092
. Fatal Errors (such as an exceeded maximum execution time) now include a
9193
backtrace.
9294
RFC: https://wiki.php.net/rfc/error_backtraces_v2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
Allow defining FCC in attributes
3+
--EXTENSIONS--
4+
reflection
5+
--FILE--
6+
<?php
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
9+
class Attr {
10+
public function __construct(public Closure $value) {
11+
var_dump($value('abc'));
12+
}
13+
}
14+
15+
#[Attr(strrev(...))]
16+
#[Attr(strlen(...))]
17+
class C {}
18+
19+
foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
20+
var_dump($reflectionAttribute->newInstance());
21+
}
22+
23+
?>
24+
--EXPECTF--
25+
string(3) "cba"
26+
object(Attr)#%d (1) {
27+
["value"]=>
28+
object(Closure)#%d (2) {
29+
["function"]=>
30+
string(6) "strrev"
31+
["parameter"]=>
32+
array(1) {
33+
["$string"]=>
34+
string(10) "<required>"
35+
}
36+
}
37+
}
38+
int(3)
39+
object(Attr)#%d (1) {
40+
["value"]=>
41+
object(Closure)#%d (2) {
42+
["function"]=>
43+
string(6) "strlen"
44+
["parameter"]=>
45+
array(1) {
46+
["$string"]=>
47+
string(10) "<required>"
48+
}
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
AST printing for FCC in attributes
3+
--FILE--
4+
<?php
5+
6+
// Do not use `false &&` to fully evaluate the function / class definition.
7+
8+
try {
9+
\assert(
10+
!
11+
#[Attr(strrev(...))]
12+
function () { }
13+
);
14+
} catch (Error $e) {
15+
echo $e->getMessage(), "\n";
16+
}
17+
18+
try {
19+
\assert(
20+
!
21+
new #[Attr(strrev(...))]
22+
class {}
23+
);
24+
} catch (Error $e) {
25+
echo $e->getMessage(), "\n";
26+
}
27+
28+
?>
29+
--EXPECT--
30+
assert(!#[Attr(strrev(...))] function () {
31+
})
32+
assert(!new #[Attr(strrev(...))] class {
33+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
AST printing for FCC in attributes at runtime
3+
--FILE--
4+
<?php
5+
6+
namespace Test;
7+
8+
class Clazz {
9+
#[Attr(strrev(...), \strrev(...), Clazz::foo(...), self::foo(...))]
10+
function foo() { }
11+
}
12+
13+
$r = new \ReflectionMethod(Clazz::class, 'foo');
14+
foreach ($r->getAttributes() as $attribute) {
15+
echo $attribute;
16+
}
17+
18+
?>
19+
--EXPECT--
20+
Attribute [ Test\Attr ] {
21+
- Arguments [4] {
22+
Argument #0 [ Test\strrev(...) ]
23+
Argument #1 [ \strrev(...) ]
24+
Argument #2 [ \Test\Clazz::foo(...) ]
25+
Argument #3 [ self::foo(...) ]
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
FCC in attribute may access private methods
3+
--EXTENSIONS--
4+
reflection
5+
--FILE--
6+
<?php
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
9+
class Attr {
10+
public function __construct(public Closure $value) {}
11+
}
12+
13+
#[Attr(C::myMethod(...))]
14+
class C {
15+
private static function myMethod(string $foo) {
16+
echo "Called ", __METHOD__, PHP_EOL;
17+
var_dump($foo);
18+
}
19+
}
20+
21+
foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
22+
($reflectionAttribute->newInstance()->value)('abc');
23+
}
24+
25+
?>
26+
--EXPECT--
27+
Called C::myMethod
28+
string(3) "abc"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
FCC in attribute may not access unrelated private methods
3+
--EXTENSIONS--
4+
reflection
5+
--FILE--
6+
<?php
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
9+
class Attr {
10+
public function __construct(public Closure $value) {}
11+
}
12+
13+
class E {
14+
private static function myMethod(string $foo) {
15+
echo "Called ", __METHOD__, PHP_EOL;
16+
var_dump($foo);
17+
}
18+
}
19+
20+
#[Attr(E::myMethod(...))]
21+
class C {
22+
}
23+
24+
foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) {
25+
($reflectionAttribute->newInstance()->value)('abc');
26+
}
27+
28+
?>
29+
--EXPECTF--
30+
Fatal error: Uncaught Error: Call to private method E::myMethod() from scope C in %s:%d
31+
Stack trace:
32+
#0 %s(%d): ReflectionAttribute->newInstance()
33+
#1 {main}
34+
thrown in %s on line %d
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
FCC in const expression triggers autoloader.
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(static function ($class) {
7+
echo "Autoloading {$class}", PHP_EOL;
8+
eval(
9+
<<<'EOT'
10+
class AutoloadedClass {
11+
public static function withStaticMethod() {
12+
echo "Called ", __METHOD__, PHP_EOL;
13+
}
14+
}
15+
EOT
16+
);
17+
});
18+
19+
const Closure = AutoloadedClass::withStaticMethod(...);
20+
21+
var_dump(Closure);
22+
(Closure)();
23+
24+
?>
25+
--EXPECTF--
26+
Autoloading AutoloadedClass
27+
object(Closure)#%d (1) {
28+
["function"]=>
29+
string(16) "withStaticMethod"
30+
}
31+
Called AutoloadedClass::withStaticMethod
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Allow defining FCC in const expressions.
3+
--FILE--
4+
<?php
5+
6+
const Closure = strrev(...);
7+
8+
var_dump(Closure);
9+
var_dump((Closure)("abc"));
10+
11+
?>
12+
--EXPECTF--
13+
object(Closure)#%d (2) {
14+
["function"]=>
15+
string(%d) "%s"
16+
["parameter"]=>
17+
array(1) {
18+
["$string"]=>
19+
string(10) "<required>"
20+
}
21+
}
22+
string(3) "cba"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Allow defining FCC in const expressions with case-insensitive names.
3+
--FILE--
4+
<?php
5+
6+
const Closure = StrRev(...);
7+
8+
var_dump(Closure);
9+
var_dump((Closure)("abc"));
10+
11+
?>
12+
--EXPECTF--
13+
object(Closure)#%d (2) {
14+
["function"]=>
15+
string(%d) "%s"
16+
["parameter"]=>
17+
array(1) {
18+
["$string"]=>
19+
string(10) "<required>"
20+
}
21+
}
22+
string(3) "cba"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Allow defining FCC in class constants.
3+
--FILE--
4+
<?php
5+
6+
class C {
7+
const Closure = strrev(...);
8+
}
9+
10+
var_dump(C::Closure);
11+
var_dump((C::Closure)("abc"));
12+
13+
?>
14+
--EXPECTF--
15+
object(Closure)#%d (2) {
16+
["function"]=>
17+
string(6) "strrev"
18+
["parameter"]=>
19+
array(1) {
20+
["$string"]=>
21+
string(10) "<required>"
22+
}
23+
}
24+
string(3) "cba"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Allow defining FCC wrapped in an array in const expressions.
3+
--FILE--
4+
<?php
5+
6+
const Closure = [strrev(...), strlen(...)];
7+
8+
var_dump(Closure);
9+
10+
foreach (Closure as $closure) {
11+
var_dump($closure("abc"));
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
array(2) {
17+
[0]=>
18+
object(Closure)#%d (2) {
19+
["function"]=>
20+
string(6) "strrev"
21+
["parameter"]=>
22+
array(1) {
23+
["$string"]=>
24+
string(10) "<required>"
25+
}
26+
}
27+
[1]=>
28+
object(Closure)#%d (2) {
29+
["function"]=>
30+
string(6) "strlen"
31+
["parameter"]=>
32+
array(1) {
33+
["$string"]=>
34+
string(10) "<required>"
35+
}
36+
}
37+
}
38+
string(3) "cba"
39+
int(3)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
FCC in default argument
3+
--FILE--
4+
<?php
5+
6+
function test(
7+
Closure $name = strrev(...)
8+
) {
9+
var_dump($name("abc"));
10+
}
11+
12+
test();
13+
test(strlen(...));
14+
15+
?>
16+
--EXPECT--
17+
string(3) "cba"
18+
int(3)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
FCC in initializer errors for FCC on abstract method
3+
--FILE--
4+
<?php
5+
6+
abstract class Foo {
7+
abstract public static function myMethod(string $foo);
8+
}
9+
10+
const Closure = Foo::myMethod(...);
11+
12+
var_dump(Closure);
13+
(Closure)("abc");
14+
15+
?>
16+
--EXPECTF--
17+
Fatal error: Uncaught Error: Cannot call abstract method Foo::myMethod() in %s:%d
18+
Stack trace:
19+
#0 {main}
20+
thrown in %s on line %d
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
FCC in initializer errors for FCC on variable.
3+
--FILE--
4+
<?php
5+
6+
const Closure = $foo(...);
7+
8+
var_dump(Closure);
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot use dynamic function name in constant expression in %s on line %d

0 commit comments

Comments
 (0)