Skip to content

Commit d0b09a7

Browse files
krakjoenikic
andcommittedJul 14, 2021
Add first-class callables
Support acquiring a Closure to a callable using the syntax func(...), $obj->method(...), etc. This is essentially a shortcut for Closure::fromCallable(). RFC: https://wiki.php.net/rfc/first_class_callable_syntax Closes GH-7019. Co-Authored-By: Nikita Popov <nikita.ppv@gmail.com>
1 parent 42cb5b5 commit d0b09a7

40 files changed

+1078
-515
lines changed
 

‎UPGRADING

+4
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ PHP 8.1 UPGRADE NOTES
197197
default values, static variable and global constant initializers, as well
198198
as attribute arguments.
199199
RFC: https://wiki.php.net/rfc/new_in_initializers
200+
. Closures for callables can now be created using the syntax `myFunc(...)`,
201+
which is the same as `Closure::fromCallable('myFunc')`. Yes, the `...` is
202+
part of the syntax, not an omission.
203+
RFC: https://wiki.php.net/rfc/first_class_callable_syntax
200204
. File uploads now provide an additional full_path key, which contains the
201205
full path (rather than just the basename) of the uploaded file. This is
202206
intended for use in conjunction with "upload webkitdirectory".

‎Zend/Optimizer/optimize_func_calls.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
188188
case ZEND_DO_ICALL:
189189
case ZEND_DO_UCALL:
190190
case ZEND_DO_FCALL_BY_NAME:
191+
case ZEND_CALLABLE_CONVERT:
191192
call--;
192193
if (call_stack[call].func && call_stack[call].opline) {
193194
zend_op *fcall = call_stack[call].opline;
@@ -216,7 +217,8 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
216217
}
217218

218219
if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level)
219-
&& call_stack[call].try_inline) {
220+
&& call_stack[call].try_inline
221+
&& opline->opcode != ZEND_CALLABLE_CONVERT) {
220222
zend_try_inline_call(op_array, fcall, opline, call_stack[call].func);
221223
}
222224
}

‎Zend/Optimizer/zend_call_graph.c

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ ZEND_API int zend_analyze_calls(zend_arena **arena, zend_script *script, uint32_
106106
case ZEND_DO_ICALL:
107107
case ZEND_DO_UCALL:
108108
case ZEND_DO_FCALL_BY_NAME:
109+
case ZEND_CALLABLE_CONVERT:
109110
func_info->flags |= ZEND_FUNC_HAS_CALLS;
110111
if (call_info) {
111112
call_info->caller_call_opline = opline;

‎Zend/Optimizer/zend_inference.c

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "zend_inference.h"
2222
#include "zend_func_info.h"
2323
#include "zend_call_graph.h"
24+
#include "zend_closures.h"
2425
#include "zend_worklist.h"
2526
#include "zend_optimizer_internal.h"
2627

@@ -3504,6 +3505,10 @@ static zend_always_inline int _zend_update_type_info(
35043505
}
35053506
}
35063507
break;
3508+
case ZEND_CALLABLE_CONVERT:
3509+
UPDATE_SSA_TYPE(MAY_BE_OBJECT | MAY_BE_RC1 | MAY_BE_RCN, ssa_op->result_def);
3510+
UPDATE_SSA_OBJ_TYPE(zend_ce_closure, /* is_instanceof */ false, ssa_op->result_def);
3511+
break;
35073512
case ZEND_FETCH_CONSTANT:
35083513
case ZEND_FETCH_CLASS_CONSTANT:
35093514
UPDATE_SSA_TYPE(MAY_BE_RC1|MAY_BE_RCN|MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY, ssa_op->result_def);
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
First Class Callable from Internal
3+
--FILE--
4+
<?php
5+
$sprintf = sprintf(...);
6+
7+
echo $sprintf("Hello %s", "World");
8+
?>
9+
--EXPECT--
10+
Hello World
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
First Class Callable from User
3+
--FILE--
4+
<?php
5+
function foo() {
6+
return "OK";
7+
}
8+
9+
$foo = foo(...);
10+
11+
echo $foo();
12+
?>
13+
--EXPECT--
14+
OK
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
First Class Callable from Method
3+
--FILE--
4+
<?php
5+
class Foo {
6+
public function bar() {
7+
echo "OK";
8+
}
9+
}
10+
11+
$foo = new Foo;
12+
$bar = $foo->bar(...);
13+
14+
echo $bar();
15+
?>
16+
--EXPECT--
17+
OK
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
First Class Callable from Private Scope
3+
--FILE--
4+
<?php
5+
class Foo {
6+
private function method() {
7+
return __METHOD__;
8+
}
9+
10+
public function bar() {
11+
return $this->method(...);
12+
}
13+
}
14+
15+
$foo = new Foo;
16+
$bar = $foo->bar(...);
17+
18+
echo ($bar())();
19+
?>
20+
--EXPECT--
21+
Foo::method
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
First Class Callable from Magic
3+
--FILE--
4+
<?php
5+
class Foo {
6+
public function __call($method, $args) {
7+
return $method;
8+
}
9+
10+
public static function __callStatic($method, $args) {
11+
return $method;
12+
}
13+
}
14+
15+
$foo = new Foo;
16+
$bar = $foo->anythingInstance(...);
17+
18+
echo $bar() . PHP_EOL;
19+
20+
$qux = Foo::anythingStatic(...);
21+
22+
echo $qux();
23+
?>
24+
--EXPECT--
25+
anythingInstance
26+
anythingStatic
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
First Class Callable from Closure
3+
--FILE--
4+
<?php
5+
$foo = function(){};
6+
$bar = $foo(...);
7+
8+
if ($foo === $bar) {
9+
echo "OK";
10+
}
11+
?>
12+
--EXPECT--
13+
OK
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
First Class Callable from Special Compiler Function
3+
--FILE--
4+
<?php
5+
$strlen = strlen(...);
6+
7+
if ($strlen("Hello World") === 11) {
8+
echo "OK";
9+
}
10+
?>
11+
--EXPECT--
12+
OK
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
First Class Callable from NEW
3+
--FILE--
4+
<?php
5+
class Foo {
6+
7+
}
8+
9+
new Foo(...);
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot create Closure for new expression in %s on line 6
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
First Class Callable from Closure::__invoke
3+
--FILE--
4+
<?php
5+
$closure = function() {
6+
return "OK";
7+
};
8+
9+
$foo = $closure->__invoke(...);
10+
11+
echo $foo();
12+
?>
13+
--EXPECT--
14+
OK
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
First Class Callable preserve Called Scope
3+
--FILE--
4+
<?php
5+
class Foo {
6+
public static function method() {
7+
return static::class;
8+
}
9+
}
10+
11+
class Bar extends Foo {}
12+
13+
$bar = Bar::method(...);
14+
15+
echo $bar();
16+
?>
17+
--EXPECT--
18+
Bar
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
First Class Callable Attribute Error
3+
--FILE--
4+
<?php
5+
#[Attribute(...)]
6+
class Foo {
7+
8+
}
9+
?>
10+
--EXPECTF--
11+
Fatal error: Cannot create Closure as attribute argument in %s on line 3
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
First class callable with nullsafe method call
3+
--FILE--
4+
<?php
5+
6+
$foo?->bar(...);
7+
8+
?>
9+
--EXPECTF--
10+
Fatal error: Cannot combine nullsafe operator with Closure creation in %s on line %d
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--TEST--
2+
First class callable with nullsafe method call (nested)
3+
--FILE--
4+
<?php
5+
6+
$foo?->foo->bar(...);
7+
8+
?>
9+
--EXPECTF--
10+
Fatal error: Cannot combine nullsafe operator with Closure creation in %s on line %d
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
First class callable with nullsafe method call (unrelated)
3+
--FILE--
4+
<?php
5+
6+
$foo = null;
7+
var_dump($foo?->foo($foo->bar(...)));
8+
9+
?>
10+
--EXPECT--
11+
NULL
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
First class callables and strict types
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
function test(int $i) {
8+
var_dump($i);
9+
}
10+
11+
require __DIR__ . '/first_class_callable_015_weak.inc';
12+
require __DIR__ . '/first_class_callable_015_strict.inc';
13+
$fn = test(...);
14+
do_weak_call($fn);
15+
try {
16+
do_strict_call($fn);
17+
} catch (Error $e) {
18+
echo $e->getMessage(), "\n";
19+
}
20+
21+
?>
22+
--EXPECTF--
23+
int(42)
24+
test(): Argument #1 ($i) must be of type int, string given, called in %s on line %d
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php declare(strict_types=1);
2+
3+
function do_strict_call(Closure $fn) {
4+
$fn("42");
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
function do_weak_call(Closure $fn) {
4+
$fn("42");
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Acquire callable to assert()
3+
--FILE--
4+
<?php
5+
6+
namespace Foo;
7+
8+
$assert = assert(...);
9+
$assert(1 == 1.0, "Message 1");
10+
try {
11+
$assert(1 == 2.0, "Message 2");
12+
} catch (\AssertionError $e) {
13+
echo $e->getMessage(), "\n";
14+
}
15+
16+
try {
17+
assert(false && strlen(...));
18+
} catch (\AssertionError $e) {
19+
echo $e->getMessage(), "\n";
20+
}
21+
22+
?>
23+
--EXPECT--
24+
Message 2
25+
assert(false && strlen(...))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
Acquire callable through various dynamic constructs
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public static function b($x) {
8+
return $x;
9+
}
10+
11+
public function c($x) {
12+
return $x;
13+
}
14+
}
15+
16+
$name = 'strlen';
17+
$fn = $name(...);
18+
var_dump($fn('x'));
19+
20+
$name = ['A', 'b'];
21+
$fn = $name(...);
22+
var_dump($fn(2));
23+
24+
$name = [new A, 'c'];
25+
$fn = $name(...);
26+
var_dump($fn(3));
27+
28+
$name1 = 'A';
29+
$name2 = 'b';
30+
$fn = $name1::$name2(...);
31+
var_dump($fn(4));
32+
33+
$name2 = 'c';
34+
$fn = (new A)->$name2(...);
35+
var_dump($fn(5));
36+
37+
$fn = [A::class, 'b'](...);
38+
var_dump($fn(6));
39+
40+
$o = new stdClass;
41+
$o->prop = A::b(...);
42+
($o->prop)(7);
43+
44+
$nam
45+
46+
?>
47+
--EXPECT--
48+
int(1)
49+
int(2)
50+
int(3)
51+
int(4)
52+
int(5)
53+
int(6)

0 commit comments

Comments
 (0)
Failed to load comments.