Skip to content

RFC: Support Closures in constant expressions #16458

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 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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)

- Curl:
. Added curl_multi_get_handles(). (timwolla)
Expand Down
4 changes: 4 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ PHP 8.5 UPGRADE NOTES
2. New Features
========================================

- Core:
. Added support for Closures in constant expressions.
RFC: https://wiki.php.net/rfc/closures_in_const_expr

- DOM:
. Added Dom\Element::$outerHTML.

Expand Down
57 changes: 57 additions & 0 deletions Zend/tests/closure_const_expr/attributes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
Allow defining closures in attributes
--EXTENSIONS--
reflection
--FILE--
<?php

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

#[Attr(static function () { })]
#[Attr(static function (...$args) {
var_dump($args);
})]
class C {}

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

?>
--EXPECTF--
object(Attr)#%d (1) {
["value"]=>
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(%d)
}
}
array(1) {
[0]=>
string(3) "foo"
}
object(Attr)#%d (1) {
["value"]=>
object(Closure)#%d (4) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(%d)
["parameter"]=>
array(1) {
["$args"]=>
string(10) "<optional>"
}
}
}
41 changes: 41 additions & 0 deletions Zend/tests/closure_const_expr/attributes_ast_printing.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
AST printing for closures in attributes
--FILE--
<?php

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

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

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

?>
--EXPECT--
assert(!#[Attr(static function ($foo) {
echo $foo;
})] function () {
})
assert(!new #[Attr(static function ($foo) {
echo $foo;
})] class {
})
28 changes: 28 additions & 0 deletions Zend/tests/closure_const_expr/attributes_scope_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Closure in attribute may access private variables
--EXTENSIONS--
reflection
--FILE--
<?php

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

#[Attr(static function (C $c) {
echo $c->secret, PHP_EOL;
})]
class C {
public function __construct(
private string $secret,
) {}
}

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

?>
--EXPECT--
secret
35 changes: 35 additions & 0 deletions Zend/tests/closure_const_expr/attributes_scope_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Closure in attribute may not access unrelated private variables
--EXTENSIONS--
reflection
--FILE--
<?php

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

#[Attr(static function (E $e) {
echo $e->secret, PHP_EOL;
})]
class C {
}

class E {
public function __construct(
private string $secret,
) {}
}

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

?>
--EXPECTF--
Fatal error: Uncaught Error: Cannot access private property E::$secret in %s:%d
Stack trace:
#0 %s(%d): C::{closure:%s:%d}(Object(E))
#1 {main}
thrown in %s on line %d
23 changes: 23 additions & 0 deletions Zend/tests/closure_const_expr/basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Allow defining Closures in const expressions.
--FILE--
<?php

const Closure = static function () {
echo "called", PHP_EOL;
};

var_dump(Closure);
(Closure)();

?>
--EXPECTF--
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(3)
}
called
25 changes: 25 additions & 0 deletions Zend/tests/closure_const_expr/class_const.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Allow defining Closures in class constants.
--FILE--
<?php

class C {
const Closure = static function () {
echo "called", PHP_EOL;
};
}

var_dump(C::Closure);
(C::Closure)();

?>
--EXPECTF--
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(4)
}
called
41 changes: 41 additions & 0 deletions Zend/tests/closure_const_expr/complex_array.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
Allow defining Closures wrapped in an array in const expressions.
--FILE--
<?php

const Closure = [static function () {
echo "called", PHP_EOL;
}, static function () {
echo "also called", PHP_EOL;
}];

var_dump(Closure);

foreach (Closure as $closure) {
$closure();
}

?>
--EXPECTF--
array(2) {
[0]=>
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(3)
}
[1]=>
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(5)
}
}
called
also called
33 changes: 33 additions & 0 deletions Zend/tests/closure_const_expr/complex_new.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Allow defining Closures passed as constructor arguments in const expressions.
--FILE--
<?php

class Dummy {
public function __construct(
public Closure $c,
) {}
}

const Closure = new Dummy(static function () {
echo "called", PHP_EOL;
});

var_dump(Closure);

(Closure->c)();

?>
--EXPECTF--
object(Dummy)#%d (1) {
["c"]=>
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(9)
}
}
called
22 changes: 22 additions & 0 deletions Zend/tests/closure_const_expr/default_args.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Closures in default argument
--FILE--
<?php

function test(
Closure $name = static function () {
echo "default", PHP_EOL;
},
) {
$name();
}

test();
test(function () {
echo "explicit", PHP_EOL;
});

?>
--EXPECT--
default
explicit
18 changes: 18 additions & 0 deletions Zend/tests/closure_const_expr/disallows_non_static.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Disallows using non-static closures.
--FILE--
<?php

class C {
public Closure $d = function () {
var_dump($this);
};
}

$foo = new C();
var_dump($foo->d);
($foo->d)();

?>
--EXPECTF--
Fatal error: Closures in constant expressions must be static in %s on line %d
17 changes: 17 additions & 0 deletions Zend/tests/closure_const_expr/disallows_using_variables.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Disallows using variables.
--FILE--
<?php

$foo = "bar";

const Closure = static function () use ($foo) {
echo $foo, PHP_EOL;
};

var_dump(Closure);
(Closure)();

?>
--EXPECTF--
Fatal error: Cannot use(...) variables in constant expression in %s on line %d
27 changes: 27 additions & 0 deletions Zend/tests/closure_const_expr/property_initializer.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Closure in property initializer
--FILE--
<?php

class C {
public Closure $d = static function () {
echo "called", PHP_EOL;
};
}

$c = new C();
var_dump($c->d);
($c->d)();


?>
--EXPECTF--
object(Closure)#%d (3) {
["name"]=>
string(%d) "{closure:%s:%d}"
["file"]=>
string(%d) "%s"
["line"]=>
int(4)
}
called
Loading
Loading