RFC: Clone with v2 (#18747)

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-07-17 21:13:42 +02:00 committed by GitHub
parent 8629256dc7
commit 7f4076bae0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 701 additions and 10 deletions

3
NEWS
View file

@ -2,6 +2,9 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.5.0alpha3
- Core:
. Add clone-with support to the clone() function. (timwolla, edorian)
- Curl:
. Add support for CURLINFO_CONN_ID in curl_getinfo() (thecaliskan)
. Add support for CURLINFO_QUEUE_TIME_T in curl_getinfo() (thecaliskan)

View file

@ -408,7 +408,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.
. The clone language construct is now a function and supports reassigning
(readonly) properties during cloning via the new $withProperties parameter.
RFC: https://wiki.php.net/rfc/clone_with_v2
- Curl:

View file

@ -0,0 +1,71 @@
--TEST--
Clone with basic
--FILE--
<?php
class Dummy { }
$x = new stdClass();
$foo = 'FOO';
$bar = new Dummy();
$array = [
'baz' => 'BAZ',
'array' => [1, 2, 3],
];
var_dump(clone $x);
var_dump(clone($x));
var_dump(clone($x, [ 'foo' => $foo, 'bar' => $bar ]));
var_dump(clone($x, $array));
var_dump(clone($x, [ 'obj' => $x ]));
var_dump(clone($x, [
'abc',
'def',
new Dummy(),
'named' => 'value',
]));
?>
--EXPECTF--
object(stdClass)#%d (0) {
}
object(stdClass)#%d (0) {
}
object(stdClass)#%d (2) {
["foo"]=>
string(3) "FOO"
["bar"]=>
object(Dummy)#%d (0) {
}
}
object(stdClass)#%d (2) {
["baz"]=>
string(3) "BAZ"
["array"]=>
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
}
object(stdClass)#%d (1) {
["obj"]=>
object(stdClass)#%d (0) {
}
}
object(stdClass)#%d (4) {
["0"]=>
string(3) "abc"
["1"]=>
string(3) "def"
["2"]=>
object(Dummy)#%d (0) {
}
["named"]=>
string(5) "value"
}

View file

@ -0,0 +1,114 @@
--TEST--
Clone with respects visiblity
--FILE--
<?php
class P {
public $a = 'default';
protected $b = 'default';
private $c = 'default';
public private(set) string $d = 'default';
public function m1() {
return clone($this, [ 'a' => 'updated A', 'b' => 'updated B', 'c' => 'updated C', 'd' => 'updated D' ]);
}
}
class C extends P {
public function m2() {
return clone($this, [ 'a' => 'updated A', 'b' => 'updated B', 'c' => 'dynamic C' ]);
}
public function m3() {
return clone($this, [ 'd' => 'inaccessible' ]);
}
}
class Unrelated {
public function m3(P $p) {
return clone($p, [ 'b' => 'inaccessible' ]);
}
}
$p = new P();
var_dump(clone($p, [ 'a' => 'updated A' ]));
var_dump($p->m1());
$c = new C();
var_dump($c->m1());
var_dump($c->m2());
try {
var_dump($c->m3());
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
try {
var_dump(clone($p, [ 'b' => 'inaccessible' ]));
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
try {
var_dump(clone($p, [ 'd' => 'inaccessible' ]));
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
try {
var_dump((new Unrelated())->m3($p));
} catch (Error $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
object(P)#%d (4) {
["a"]=>
string(9) "updated A"
["b":protected]=>
string(7) "default"
["c":"P":private]=>
string(7) "default"
["d"]=>
string(7) "default"
}
object(P)#%d (4) {
["a"]=>
string(9) "updated A"
["b":protected]=>
string(9) "updated B"
["c":"P":private]=>
string(9) "updated C"
["d"]=>
string(9) "updated D"
}
object(C)#%d (4) {
["a"]=>
string(9) "updated A"
["b":protected]=>
string(9) "updated B"
["c":"P":private]=>
string(9) "updated C"
["d"]=>
string(9) "updated D"
}
Deprecated: Creation of dynamic property C::$c is deprecated in %s on line %d
object(C)#%d (5) {
["a"]=>
string(9) "updated A"
["b":protected]=>
string(9) "updated B"
["c":"P":private]=>
string(7) "default"
["d"]=>
string(7) "default"
["c"]=>
string(9) "dynamic C"
}
Error: Cannot modify private(set) property P::$d from scope C
Error: Cannot access protected property P::$b
Error: Cannot modify private(set) property P::$d from global scope
Error: Cannot access protected property P::$b

View file

@ -0,0 +1,23 @@
--TEST--
Clone with supports property hooks
--FILE--
<?php
class Clazz {
public string $hooked = 'default' {
set {
$this->hooked = strtoupper($value);
}
}
}
$c = new Clazz();
var_dump(clone($c, [ 'hooked' => 'updated' ]));
?>
--EXPECTF--
object(Clazz)#%d (1) {
["hooked"]=>
string(7) "UPDATED"
}

View file

@ -0,0 +1,82 @@
--TEST--
Clone with evaluation order
--FILE--
<?php
class Clazz {
public string $hooked = 'default' {
set {
echo __FUNCTION__, PHP_EOL;
$this->hooked = strtoupper($value);
}
}
public string $maxLength {
set {
echo __FUNCTION__, PHP_EOL;
if (strlen($value) > 5) {
throw new \Exception('Length exceeded');
}
$this->maxLength = $value;
}
}
public string $minLength {
set {
echo __FUNCTION__, PHP_EOL;
if (strlen($value) < 5) {
throw new \Exception('Length unsufficient');
}
$this->minLength = $value;
}
}
}
$c = new Clazz();
var_dump(clone($c, [ 'hooked' => 'updated' ]));
echo PHP_EOL;
var_dump(clone($c, [ 'hooked' => 'updated', 'maxLength' => 'abc', 'minLength' => 'abcdef' ]));
echo PHP_EOL;
var_dump(clone($c, [ 'minLength' => 'abcdef', 'hooked' => 'updated', 'maxLength' => 'abc' ]));
?>
--EXPECTF--
$hooked::set
object(Clazz)#%d (1) {
["hooked"]=>
string(7) "UPDATED"
["maxLength"]=>
uninitialized(string)
["minLength"]=>
uninitialized(string)
}
$hooked::set
$maxLength::set
$minLength::set
object(Clazz)#%d (3) {
["hooked"]=>
string(7) "UPDATED"
["maxLength"]=>
string(3) "abc"
["minLength"]=>
string(6) "abcdef"
}
$minLength::set
$hooked::set
$maxLength::set
object(Clazz)#%d (3) {
["hooked"]=>
string(7) "UPDATED"
["maxLength"]=>
string(3) "abc"
["minLength"]=>
string(6) "abcdef"
}

View file

@ -0,0 +1,64 @@
--TEST--
Clone with error handling
--FILE--
<?php
class Clazz {
public string $hooked = 'default' {
set {
echo __FUNCTION__, PHP_EOL;
$this->hooked = strtoupper($value);
}
}
public string $maxLength {
set {
echo __FUNCTION__, PHP_EOL;
if (strlen($value) > 5) {
throw new \Exception('Length exceeded');
}
$this->maxLength = $value;
}
}
public string $minLength {
set {
echo __FUNCTION__, PHP_EOL;
if (strlen($value) < 5) {
throw new \Exception('Length insufficient');
}
$this->minLength = $value;
}
}
}
$c = new Clazz();
try {
var_dump(clone($c, [ 'hooked' => 'updated', 'maxLength' => 'abcdef', 'minLength' => 'abc' ]));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
echo PHP_EOL;
try {
var_dump(clone($c, [ 'hooked' => 'updated', 'minLength' => 'abc', 'maxLength' => 'abcdef' ]));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
$hooked::set
$maxLength::set
Exception: Length exceeded
$hooked::set
$minLength::set
Exception: Length insufficient

View file

@ -0,0 +1,16 @@
--TEST--
Clone with error cases
--FILE--
<?php
$x = new stdClass();
try {
var_dump(clone($x, 1));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
TypeError: clone(): Argument #2 ($withProperties) must be of type array, int given

View file

@ -0,0 +1,29 @@
--TEST--
Clone with supports __clone
--FILE--
<?php
class Clazz {
public function __construct(
public string $foo,
public string $bar,
) { }
public function __clone() {
$this->foo = 'foo updated in __clone';
$this->bar = 'bar updated in __clone';
}
}
$c = new Clazz('foo', 'bar');
var_dump(clone($c, [ 'foo' => 'foo updated in clone-with' ]));
?>
--EXPECTF--
object(Clazz)#%d (2) {
["foo"]=>
string(25) "foo updated in clone-with"
["bar"]=>
string(22) "bar updated in __clone"
}

View file

@ -0,0 +1,40 @@
--TEST--
Clone with readonly
--FILE--
<?php
readonly class Clazz {
public function __construct(
public public(set) string $a,
public public(set) string $b,
) { }
public function __clone() {
$this->b = '__clone';
}
}
$c = new Clazz('default', 'default');
var_dump(clone($c, [ 'a' => "with" ]));
try {
var_dump(clone($c, [ 'b' => "with" ]));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
object(Clazz)#%d (2) {
["a"]=>
string(4) "with"
["b"]=>
string(7) "__clone"
}
object(Clazz)#%d (2) {
["a"]=>
string(7) "default"
["b"]=>
string(4) "with"
}

View file

@ -0,0 +1,72 @@
--TEST--
Clone with lazy objects
--FILE--
<?php
class C {
public $a = 1;
public function __construct() {
}
}
function test(string $name, object $obj) {
printf("# %s:\n", $name);
$reflector = new ReflectionClass($obj::class);
$clone = clone($obj, [ 'a' => 2 ]);
var_dump($reflector->isUninitializedLazyObject($obj));
var_dump($obj);
var_dump($reflector->isUninitializedLazyObject($clone));
var_dump($clone);
}
$reflector = new ReflectionClass(C::class);
$obj = $reflector->newLazyGhost(function ($obj) {
var_dump("initializer");
$obj->__construct();
});
test('Ghost', $obj);
$obj = $reflector->newLazyProxy(function ($obj) {
var_dump("initializer");
return new C();
});
test('Proxy', $obj);
?>
--EXPECTF--
# Ghost:
string(11) "initializer"
bool(false)
object(C)#%d (1) {
["a"]=>
int(1)
}
bool(false)
object(C)#%d (1) {
["a"]=>
int(2)
}
# Proxy:
string(11) "initializer"
bool(false)
lazy proxy object(C)#%d (1) {
["instance"]=>
object(C)#%d (1) {
["a"]=>
int(1)
}
}
bool(false)
lazy proxy object(C)#%d (1) {
["instance"]=>
object(C)#%d (1) {
["a"]=>
int(2)
}
}

View file

@ -0,0 +1,21 @@
--TEST--
Clone with native classes
--FILE--
<?php
try {
var_dump(clone(new \Random\Engine\Secure(), [ 'with' => "something" ]));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
try {
var_dump(clone(new \Random\Engine\Xoshiro256StarStar(), [ 'with' => "something" ]));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
Error: Trying to clone an uncloneable object of class Random\Engine\Secure
Error: Cannot create dynamic property Random\Engine\Xoshiro256StarStar::$with

View file

@ -0,0 +1,18 @@
--TEST--
Clone with name mangling
--FILE--
<?php
class Foo {
private string $bar = 'default';
}
try {
var_dump(clone(new Foo(), ["\0Foo\0bar" => 'updated']));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECT--
Error: Cannot access property starting with "\0"

View file

@ -0,0 +1,35 @@
--TEST--
Clone with property hook updating readonly property
--FILE--
<?php
class Clazz {
public string $foo {
set {
$this->foo = $value;
$this->bar = 'bar updated in hook';
}
}
public public(set) readonly string $bar;
}
$f = new Clazz();
var_dump(clone($f, ['foo' => 'foo updated in clone-with']));
try {
var_dump(clone($f, ['foo' => 'foo updated in clone-with', 'bar' => 'bar updated in clone-with']));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
object(Clazz)#%d (2) {
["foo"]=>
string(25) "foo updated in clone-with"
["bar"]=>
string(19) "bar updated in hook"
}
Error: Cannot modify readonly property Clazz::$bar

View file

@ -0,0 +1,31 @@
--TEST--
Clone with references
--FILE--
<?php
$x = new stdClass();
$ref = 'reference';
$with = ['x' => &$ref];
try {
var_dump(clone($x, $with));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
unset($ref);
try {
var_dump(clone($x, $with));
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
}
?>
--EXPECTF--
Error: Cannot assign by reference when cloning with updated properties
object(stdClass)#%d (1) {
["x"]=>
string(9) "reference"
}

View file

@ -72,9 +72,12 @@ zend_result zend_startup_builtin_functions(void) /* {{{ */
ZEND_FUNCTION(clone)
{
zend_object *zobj;
HashTable *with = (HashTable*)&zend_empty_array;
ZEND_PARSE_PARAMETERS_START(1, 1)
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_OBJ(zobj)
Z_PARAM_OPTIONAL
Z_PARAM_ARRAY_HT(with)
ZEND_PARSE_PARAMETERS_END();
/* clone() also exists as the ZEND_CLONE OPcode and both implementations must be kept in sync. */
@ -95,7 +98,16 @@ ZEND_FUNCTION(clone)
}
zend_object *cloned;
cloned = zobj->handlers->clone_obj(zobj);
if (zend_hash_num_elements(with) > 0) {
if (UNEXPECTED(!zobj->handlers->clone_obj_with)) {
zend_throw_error(NULL, "Cloning objects of class %s with updated properties is not supported", ZSTR_VAL(ce->name));
RETURN_THROWS();
}
cloned = zobj->handlers->clone_obj_with(zobj, scope, with);
} else {
cloned = zobj->handlers->clone_obj(zobj);
}
ZEND_ASSERT(cloned || EG(exception));
if (EXPECTED(cloned)) {

View file

@ -8,7 +8,7 @@ class stdClass
}
/** @refcount 1 */
function _clone(object $object): object {}
function _clone(object $object, array $withProperties = []): object {}
function exit(string|int $status = 0): never {}

View file

@ -1,8 +1,9 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 12327caa3fe940ccef68ed99f9278982dc0173a5 */
* Stub hash: 0be87bb6b55e100c022e70aa6f3b17001725784f */
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_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, withProperties, IS_ARRAY, 0, "[]")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_exit, 0, 0, IS_NEVER, 0)

View file

@ -31,6 +31,7 @@ static const zend_object_handlers iterator_object_handlers = {
iter_wrapper_free,
iter_wrapper_dtor,
NULL, /* clone_obj */
NULL, /* clone_obj_with */
NULL, /* prop read */
NULL, /* prop write */
NULL, /* read dim */

View file

@ -2541,6 +2541,7 @@ ZEND_API const zend_object_handlers std_object_handlers = {
zend_object_std_dtor, /* free_obj */
zend_objects_destroy_object, /* dtor_obj */
zend_objects_clone_obj, /* clone_obj */
zend_objects_clone_obj_with, /* clone_obj_with */
zend_std_read_property, /* read_property */
zend_std_write_property, /* write_property */

View file

@ -180,6 +180,7 @@ typedef void (*zend_object_free_obj_t)(zend_object *object);
typedef void (*zend_object_dtor_obj_t)(zend_object *object);
typedef zend_object* (*zend_object_clone_obj_t)(zend_object *object);
typedef zend_object* (*zend_object_clone_obj_with_t)(zend_object *object, const zend_class_entry *scope, const HashTable *properties);
/* Get class name for display in var_dump and other debugging functions.
* Must be defined and must return a non-NULL value. */
@ -209,6 +210,7 @@ struct _zend_object_handlers {
zend_object_free_obj_t free_obj; /* required */
zend_object_dtor_obj_t dtor_obj; /* required */
zend_object_clone_obj_t clone_obj; /* optional */
zend_object_clone_obj_with_t clone_obj_with; /* optional */
zend_object_read_property_t read_property; /* required */
zend_object_write_property_t write_property; /* required */
zend_object_read_dimension_t read_dimension; /* required */

View file

@ -276,6 +276,52 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
}
}
ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *old_object, const zend_class_entry *scope, const HashTable *properties)
{
zend_object *new_object = old_object->handlers->clone_obj(old_object);
if (EXPECTED(!EG(exception))) {
/* Unlock readonly properties once more. */
if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce)) {
for (uint32_t i = 0; i < new_object->ce->default_properties_count; i++) {
zval* prop = OBJ_PROP_NUM(new_object, i);
Z_PROP_FLAG_P(prop) |= IS_PROP_REINITABLE;
}
}
const zend_class_entry *old_scope = EG(fake_scope);
EG(fake_scope) = scope;
ZEND_HASH_FOREACH_KEY_VAL(properties, zend_ulong num_key, zend_string *key, zval *val) {
if (UNEXPECTED(Z_ISREF_P(val))) {
if (Z_REFCOUNT_P(val) == 1) {
val = Z_REFVAL_P(val);
} else {
zend_throw_error(NULL, "Cannot assign by reference when cloning with updated properties");
break;
}
}
if (UNEXPECTED(key == NULL)) {
key = zend_long_to_str(num_key);
new_object->handlers->write_property(new_object, key, val, NULL);
zend_string_release_ex(key, false);
} else {
new_object->handlers->write_property(new_object, key, val, NULL);
}
if (UNEXPECTED(EG(exception))) {
break;
}
} ZEND_HASH_FOREACH_END();
EG(fake_scope) = old_scope;
}
return new_object;
}
ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object)
{
zend_object *new_object;

View file

@ -30,6 +30,7 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
ZEND_API void zend_object_std_dtor(zend_object *object);
ZEND_API void zend_objects_destroy_object(zend_object *object);
ZEND_API zend_object *zend_objects_clone_obj(zend_object *object);
ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *object, const zend_class_entry *scope, const HashTable *properties);
void zend_object_dtor_dynamic_properties(zend_object *object);
void zend_object_dtor_property(zend_object *object, zval *p);

View file

@ -6006,7 +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. */
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync.
* The OPcode intentionally does not support a clone-with property list to keep it simple. */
do {
if (OP1_TYPE == IS_CONST ||

12
Zend/zend_vm_execute.h generated
View file

@ -5180,7 +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. */
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync.
* The OPcode intentionally does not support a clone-with property list to keep it simple. */
do {
if (IS_CONST == IS_CONST ||
@ -15427,7 +15428,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. */
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync.
* The OPcode intentionally does not support a clone-with property list to keep it simple. */
do {
if ((IS_TMP_VAR|IS_VAR) == IS_CONST ||
@ -33522,7 +33524,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. */
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync.
* The OPcode intentionally does not support a clone-with property list to keep it simple. */
do {
if (IS_UNUSED == IS_CONST ||
@ -41041,7 +41044,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. */
/* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync.
* The OPcode intentionally does not support a clone-with property list to keep it simple. */
do {
if (IS_CV == IS_CONST ||

View file

@ -514,6 +514,7 @@ zend_object_handlers php_com_object_handlers = {
php_com_object_free_storage,
zend_objects_destroy_object,
php_com_object_clone,
NULL, /* clone_with */
com_property_read,
com_property_write,
com_read_dimension,

View file

@ -402,6 +402,7 @@ zend_object_handlers php_com_saproxy_handlers = {
saproxy_free_storage,
zend_objects_destroy_object,
saproxy_clone,
NULL, /* clone_with */
saproxy_property_read,
saproxy_property_write,
saproxy_read_dimension,