Skip to content

Commit b27448a

Browse files
committed
Support first-class callables in const-expressions
1 parent 4d140f7 commit b27448a

24 files changed

+649
-1
lines changed
Lines changed: 50 additions & 0 deletions
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+
}
Lines changed: 33 additions & 0 deletions
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+
})
Lines changed: 28 additions & 0 deletions
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"
Lines changed: 34 additions & 0 deletions
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
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Errors if the function does not exist.
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
Lines changed: 22 additions & 0 deletions
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"
Lines changed: 24 additions & 0 deletions
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"
Lines changed: 39 additions & 0 deletions
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)
Lines changed: 18 additions & 0 deletions
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)
Lines changed: 12 additions & 0 deletions
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
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
FCC in initializer errors for FCC on Closure.
3+
--FILE--
4+
<?php
5+
6+
const Closure = (static function () { })(...);
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
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
FCC in initializer errors for FCC on Constant.
3+
--FILE--
4+
<?php
5+
6+
const Name = 'strrev';
7+
8+
const Closure = (Name)(...);
9+
10+
var_dump(Closure);
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Cannot use dynamic function name in constant expression in %s on line %d
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
FCC in initializer errors for FCC on instance call.
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function myMethod(string $foo) {
8+
echo "Called ", __METHOD__, PHP_EOL;
9+
var_dump($foo);
10+
}
11+
}
12+
13+
const Closure = (new Foo())->myMethod(...);
14+
15+
var_dump(Closure);
16+
(Closure)("abc");
17+
18+
?>
19+
--EXPECTF--
20+
Fatal error: Constant expression contains invalid operations in %s on line %d
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
FCC in initializer errors for missing class.
3+
--FILE--
4+
<?php
5+
6+
const Closure = ThisClassNotDoesExist::thisMethodIsNotRelevant(...);
7+
8+
var_dump(Closure);
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Uncaught Error: Class "ThisClassNotDoesExist" not found in %s:%d
13+
Stack trace:
14+
#0 {main}
15+
thrown in %s on line %d
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
FCC in initializer errors for missing function.
3+
--FILE--
4+
<?php
5+
6+
const Closure = this_function_does_not_exist(...);
7+
8+
var_dump(Closure);
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Uncaught Error: Call to undefined function this_function_does_not_exist() in %s:%d
13+
Stack trace:
14+
#0 {main}
15+
thrown in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
FCC in initializer errors for missing method.
3+
--FILE--
4+
<?php
5+
6+
class ThisClassDoesExist { }
7+
8+
const Closure = ThisClassDoesExist::thisMethodDoesNotExist(...);
9+
10+
var_dump(Closure);
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Uncaught Error: Call to undefined method ThisClassDoesExist::thisMethodDoesNotExist() in %s:%d
15+
Stack trace:
16+
#0 {main}
17+
thrown in %s on line %d

0 commit comments

Comments
 (0)