Skip to content

Commit

Permalink
Support Closures in constant expressions (php#16458)
Browse files Browse the repository at this point in the history
RFC: https://wiki.php.net/rfc/closures_in_const_expr

Co-authored-by: Volker Dusch <volker@tideways-gmbh.com>
Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
Co-authored-by: Arthur Kurbidaev <artkurbidaev@gmail.com>
  • Loading branch information
4 people authored Dec 2, 2024
1 parent 3b517e0 commit f6a0bb4
Show file tree
Hide file tree
Showing 24 changed files with 577 additions and 26 deletions.
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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

0 comments on commit f6a0bb4

Please sign in to comment.