1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

RFC: Turn clone() into a function (#18919)

RFC: https://wiki.php.net/rfc/clone_with_v2

Co-authored-by: Volker Dusch <volker@tideways-gmbh.com>
This commit is contained in:
Tim Düsterhus
2025-06-24 20:14:40 +02:00
committed by GitHub
parent 5ed8b2be55
commit ca49a7bec2
25 changed files with 227 additions and 58 deletions

1
NEWS
View File

@@ -59,6 +59,7 @@ PHP NEWS
. Added support for `final` with constructor property promotion.
(DanielEScherzer)
. Do not use RTLD_DEEPBIND if dlmopen is available. (Daniil Gentili)
. Make `clone() a function. (timwolla, edorian)
- Curl:
. Added curl_multi_get_handles(). (timwolla)

View File

@@ -374,6 +374,8 @@ PHP 8.5 UPGRADE NOTES
. get_exception_handler() allows retrieving the current user-defined exception
handler function.
RFC: https://wiki.php.net/rfc/get-error-exception-handler
. The clone language construct is now a function.
RFC: https://wiki.php.net/rfc/clone_with_v2
- Curl:
. curl_multi_get_handles() allows retrieving all CurlHandles current

View File

@@ -1,6 +1,7 @@
/* This is a generated file, edit the .stub.php files instead. */
static const func_info_t func_infos[] = {
F1("clone", MAY_BE_OBJECT),
F1("zend_version", MAY_BE_STRING),
FN("func_get_args", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ANY),
F1("get_class_vars", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF),

View File

@@ -183,7 +183,7 @@ assert(0 && ($a = function () {
$x = $a ?? $b;
[$a, $b, $c] = [1, 2 => 'x', 'z' => 'c'];
@foo();
$y = clone $x;
$y = \clone($x);
yield 1 => 2;
yield from $x;
}))

31
Zend/tests/clone/ast.phpt Normal file
View File

@@ -0,0 +1,31 @@
--TEST--
Ast Printing
--FILE--
<?php
$x = new stdClass();
try {
assert(false && $y = clone $x);
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && $y = clone($x));
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
assert(false && $y = clone(...));
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
assert(false && ($y = \clone($x)))
assert(false && ($y = \clone($x)))
assert(false && ($y = \clone(...)))

View File

@@ -4,11 +4,11 @@ Bug #36071 (Engine Crash related with 'clone')
error_reporting=4095
--FILE--
<?php
$a = clone 0;
$a[0]->b = 0;
try {
$a = clone 0;
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Fatal error: Uncaught Error: __clone method called on non-object in %sbug36071.php:2
Stack trace:
#0 {main}
thrown in %sbug36071.php on line 2
--EXPECT--
TypeError: clone(): Argument #1 ($object) must be of type object, int given

View File

@@ -2,11 +2,11 @@
Bug #42817 (clone() on a non-object does not result in a fatal error)
--FILE--
<?php
$a = clone(null);
array_push($a->b, $c);
try {
$a = clone(null);
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Fatal error: Uncaught Error: __clone method called on non-object in %sbug42817.php:2
Stack trace:
#0 {main}
thrown in %sbug42817.php on line 2
--EXPECT--
TypeError: clone(): Argument #1 ($object) must be of type object, null given

View File

@@ -2,10 +2,11 @@
Bug #42818 ($foo = clone(array()); leaks memory)
--FILE--
<?php
$foo = clone(array());
try {
$foo = clone(array());
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Fatal error: Uncaught Error: __clone method called on non-object in %sbug42818.php:2
Stack trace:
#0 {main}
thrown in %sbug42818.php on line 2
--EXPECT--
TypeError: clone(): Argument #1 ($object) must be of type object, array given

View File

@@ -3,11 +3,12 @@ Using clone statement on non-object
--FILE--
<?php
$a = clone array();
try {
$a = clone array();
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Fatal error: Uncaught Error: __clone method called on non-object in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
--EXPECT--
TypeError: clone(): Argument #1 ($object) must be of type object, array given

View File

@@ -3,13 +3,13 @@ Using clone statement on undefined variable
--FILE--
<?php
$a = clone $b;
try {
$a = clone $b;
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Warning: Undefined variable $b in %s on line %d
Fatal error: Uncaught Error: __clone method called on non-object in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
TypeError: clone(): Argument #1 ($object) must be of type object, null given

View File

@@ -0,0 +1,55 @@
--TEST--
Clone as a function.
--FILE--
<?php
$x = new stdClass();
\var_dump(\clone($x));
\var_dump(\array_map('clone', [$x, $x, $x]));
\var_dump(\array_map(clone(...), [$x, $x, $x]));
class Foo {
private function __clone() {
}
public function clone_me() {
// Verify visibility when going through array_map().
return array_map(\clone(...), [$this]);
}
}
$f = new Foo();
$clone = $f->clone_me()[0];
var_dump($f !== $clone);
?>
--EXPECTF--
object(stdClass)#%d (0) {
}
array(3) {
[0]=>
object(stdClass)#%d (0) {
}
[1]=>
object(stdClass)#%d (0) {
}
[2]=>
object(stdClass)#%d (0) {
}
}
array(3) {
[0]=>
object(stdClass)#%d (0) {
}
[1]=>
object(stdClass)#%d (0) {
}
[2]=>
object(stdClass)#%d (0) {
}
}
bool(true)

View File

@@ -23,6 +23,7 @@ function test_clone() {
$b = clone $c->x;
}
// No catch, because we want to test Exception::__toString().
test_clone();
?>
--EXPECTF--

View File

@@ -3648,6 +3648,7 @@ static void zend_disable_function(const char *function_name, size_t function_nam
if (UNEXPECTED(
(function_name_length == strlen("exit") && !memcmp(function_name, "exit", strlen("exit")))
|| (function_name_length == strlen("die") && !memcmp(function_name, "die", strlen("die")))
|| (function_name_length == strlen("clone") && !memcmp(function_name, "clone", strlen("clone")))
)) {
zend_error(E_WARNING, "Cannot disable function %s()", function_name);
return;

View File

@@ -2345,8 +2345,6 @@ simple_list:
}
smart_str_appendc(str, '`');
break;
case ZEND_AST_CLONE:
PREFIX_OP("clone ", 270, 271);
case ZEND_AST_PRINT:
PREFIX_OP("print ", 60, 61);
case ZEND_AST_INCLUDE_OR_EVAL:

View File

@@ -89,7 +89,6 @@ enum _zend_ast_kind {
ZEND_AST_ISSET,
ZEND_AST_SILENCE,
ZEND_AST_SHELL_EXEC,
ZEND_AST_CLONE,
ZEND_AST_PRINT,
ZEND_AST_INCLUDE_OR_EVAL,
ZEND_AST_UNARY_OP,

View File

@@ -69,6 +69,49 @@ zend_result zend_startup_builtin_functions(void) /* {{{ */
}
/* }}} */
ZEND_FUNCTION(clone)
{
zend_object *zobj;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_OBJ(zobj)
ZEND_PARSE_PARAMETERS_END();
/* clone() also exists as the ZEND_CLONE OPcode and both implementations must be kept in sync. */
zend_class_entry *scope = zend_get_executed_scope();
zend_class_entry *ce = zobj->ce;
zend_function *clone = ce->clone;
if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) {
zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name));
RETURN_THROWS();
}
if (clone && !(clone->common.fn_flags & ZEND_ACC_PUBLIC)) {
if (clone->common.scope != scope) {
if (UNEXPECTED(clone->common.fn_flags & ZEND_ACC_PRIVATE)
|| UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) {
zend_throw_error(NULL, "Call to %s %s::__clone() from %s%s",
zend_visibility_string(clone->common.fn_flags), ZSTR_VAL(clone->common.scope->name),
scope ? "scope " : "global scope",
scope ? ZSTR_VAL(scope->name) : ""
);
RETURN_THROWS();
}
}
}
zend_object *cloned;
cloned = zobj->handlers->clone_obj(zobj);
ZEND_ASSERT(cloned || EG(exception));
if (EXPECTED(cloned)) {
RETURN_OBJ(cloned);
}
}
ZEND_FUNCTION(exit)
{
zend_string *str = NULL;

View File

@@ -7,6 +7,9 @@ class stdClass
{
}
/** @refcount 1 */
function _clone(object $object): object {}
function exit(string|int $status = 0): never {}
/** @alias exit */

View File

@@ -1,5 +1,9 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: a24761186f1ddf758e648b0a764826537cbd33b9 */
* Stub hash: 12327caa3fe940ccef68ed99f9278982dc0173a5 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0)
ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_exit, 0, 0, IS_NEVER, 0)
ZEND_ARG_TYPE_MASK(0, status, MAY_BE_STRING|MAY_BE_LONG, "0")
@@ -243,6 +247,7 @@ static const zend_frameless_function_info frameless_function_infos_class_exists[
{ 0 },
};
ZEND_FUNCTION(clone);
ZEND_FUNCTION(exit);
ZEND_FUNCTION(zend_version);
ZEND_FUNCTION(func_num_args);
@@ -306,6 +311,7 @@ ZEND_FUNCTION(gc_disable);
ZEND_FUNCTION(gc_status);
static const zend_function_entry ext_functions[] = {
ZEND_FE(clone, arginfo_clone)
ZEND_FE(exit, arginfo_exit)
ZEND_RAW_FENTRY("die", zif_exit, arginfo_die, 0, NULL, NULL)
ZEND_FE(zend_version, arginfo_zend_version)

View File

@@ -4930,6 +4930,20 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
return SUCCESS;
}
static zend_result zend_compile_func_clone(znode *result, zend_ast_list *args)
{
znode arg_node;
if (args->children != 1) {
return FAILURE;
}
zend_compile_expr(&arg_node, args->child[0]);
zend_emit_op_tmp(result, ZEND_CLONE, &arg_node, NULL);
return SUCCESS;
}
static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, zend_function *fbc, uint32_t type) /* {{{ */
{
if (zend_string_equals_literal(lcname, "strlen")) {
@@ -4998,6 +5012,8 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
return zend_compile_func_array_key_exists(result, args);
} else if (zend_string_equals_literal(lcname, "sprintf")) {
return zend_compile_func_sprintf(result, args);
} else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) {
return zend_compile_func_clone(result, args);
} else {
return FAILURE;
}
@@ -5391,17 +5407,6 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */
}
/* }}} */
static void zend_compile_clone(znode *result, zend_ast *ast) /* {{{ */
{
zend_ast *obj_ast = ast->child[0];
znode obj_node;
zend_compile_expr(&obj_node, obj_ast);
zend_emit_op_tmp(result, ZEND_CLONE, &obj_node, NULL);
}
/* }}} */
static void zend_compile_global_var(zend_ast *ast) /* {{{ */
{
zend_ast *var_ast = ast->child[0];
@@ -11717,9 +11722,6 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
case ZEND_AST_NEW:
zend_compile_new(result, ast);
return;
case ZEND_AST_CLONE:
zend_compile_clone(result, ast);
return;
case ZEND_AST_ASSIGN_OP:
zend_compile_compound_assign(result, ast);
return;

View File

@@ -1228,7 +1228,16 @@ expr:
{ $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); }
| variable '=' ampersand variable
{ $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); }
| T_CLONE expr { $$ = zend_ast_create(ZEND_AST_CLONE, $2); }
| T_CLONE '(' T_ELLIPSIS ')' {
zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE));
name->attr = ZEND_NAME_FQ;
$$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_fcc());
}
| T_CLONE expr {
zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE));
name->attr = ZEND_NAME_FQ;
$$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2));
}
| variable T_PLUS_EQUAL expr
{ $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); }
| variable T_MINUS_EQUAL expr

View File

@@ -575,6 +575,7 @@ EMPTY_SWITCH_DEFAULT_CASE()
_(ZEND_STR_UNKNOWN, "unknown") \
_(ZEND_STR_UNKNOWN_CAPITALIZED, "Unknown") \
_(ZEND_STR_EXIT, "exit") \
_(ZEND_STR_CLONE, "clone") \
_(ZEND_STR_EVAL, "eval") \
_(ZEND_STR_INCLUDE, "include") \
_(ZEND_STR_REQUIRE, "require") \

View File

@@ -6006,6 +6006,8 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY)
SAVE_OPLINE();
obj = GET_OP1_OBJ_ZVAL_PTR_UNDEF(BP_VAR_R);
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */
do {
if (OP1_TYPE == IS_CONST ||
(OP1_TYPE != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) {
@@ -6022,7 +6024,7 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY)
HANDLE_EXCEPTION();
}
}
zend_throw_error(NULL, "__clone method called on non-object");
zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj));
FREE_OP1();
HANDLE_EXCEPTION();
}

16
Zend/zend_vm_execute.h generated
View File

@@ -5180,6 +5180,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_
SAVE_OPLINE();
obj = RT_CONSTANT(opline, opline->op1);
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */
do {
if (IS_CONST == IS_CONST ||
(IS_CONST != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) {
@@ -5196,7 +5198,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_
HANDLE_EXCEPTION();
}
}
zend_throw_error(NULL, "__clone method called on non-object");
zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj));
HANDLE_EXCEPTION();
}
@@ -15428,6 +15430,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND
SAVE_OPLINE();
obj = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC);
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */
do {
if ((IS_TMP_VAR|IS_VAR) == IS_CONST ||
((IS_TMP_VAR|IS_VAR) != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) {
@@ -15444,7 +15448,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND
HANDLE_EXCEPTION();
}
}
zend_throw_error(NULL, "__clone method called on non-object");
zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj));
zval_ptr_dtor_nogc(EX_VAR(opline->op1.var));
HANDLE_EXCEPTION();
}
@@ -33523,6 +33527,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND
SAVE_OPLINE();
obj = &EX(This);
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */
do {
if (IS_UNUSED == IS_CONST ||
(IS_UNUSED != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) {
@@ -33539,7 +33545,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND
HANDLE_EXCEPTION();
}
}
zend_throw_error(NULL, "__clone method called on non-object");
zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj));
HANDLE_EXCEPTION();
}
@@ -41042,6 +41048,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC
SAVE_OPLINE();
obj = EX_VAR(opline->op1.var);
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */
do {
if (IS_CV == IS_CONST ||
(IS_CV != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) {
@@ -41058,7 +41066,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC
HANDLE_EXCEPTION();
}
}
zend_throw_error(NULL, "__clone method called on non-object");
zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj));
HANDLE_EXCEPTION();
}

View File

@@ -1009,6 +1009,9 @@ class FunctionName implements FunctionOrMethodName {
private /* readonly */ Name $name;
public function __construct(Name $name) {
if ($name->name === '_clone') {
$name = new Name('clone', $name->getAttributes());
}
$this->name = $name;
}
@@ -3049,6 +3052,7 @@ class PropertyInfo extends VariableLike
"parent" => "ZEND_STR_PARENT",
"username" => "ZEND_STR_USERNAME",
"password" => "ZEND_STR_PASSWORD",
"clone" => "ZEND_STR_CLONE",
];
/**

View File

@@ -16,7 +16,7 @@ foreach (get_defined_functions()["internal"] as $function) {
if (in_array($function, ["extract", "compact", "get_defined_vars"])) {
continue;
}
$contents .= " \$result = {$function}();\n";
$contents .= " \$result = \\{$function}();\n";
}
$contents .= "}\n";