====== PHP RFC: Clone with v2 ======
* Version: 1.1
* Date: 2025-03-30
* Author: Volker Dusch (volker@tideways-gmbh.com), Tim Düsterhus (tim@tideways-gmbh.com)
* Status: Voting
* First Published at: https://wiki.php.net/rfc/clone_with_v2
===== Introduction =====
Both readonly properties and the cloning of objects are commonplace in a lot of PHP applications, but these two concepts don’t fully work together in all cases.
This RFC proposes to address this issue by allowing the clone language construct to work well with readonly properties.
One common usage pattern for object cloning is “immutable value-objects” having ->withProperty() methods returning a new object with some property changed. This doesn’t work smoothly with readonly properties at the moment.
To address this, we propose what we consider a simple change that makes clone look and mostly behave like a regular function call. This will align clone with current PHP users' expectations regarding syntax and semantics, while still respecting property visibility rules.
public function withStatus($code, $reasonPhrase = ''): Response {
return clone($this, [
"statusCode" => $code,
"reasonPhrase" => $reasonPhrase,
]);
}
The main reason we think a clone() function that directly updates the properties is preferable to one that passes the updated properties to the magic __clone() method of an object, is that it allows objects with public properties to be cloned with changed properties from the outside without assistance by the object. Furthermore, within a class changes can be localized in appropriate methods rather than centralized in the __clone() method.
===== Prior Works =====
This change was proposed in https://wiki.php.net/rfc/clone_with and discussed https://externals.io/message/120048
Máté, the original RFC author, dropped the RFC and had no objections to us proposing this continuation trying to address the same need.
===== Proposal =====
We propose to change clone from a standalone keyword to a language construct that optionally accepts a second array parameter.
function clone(object $object, array $withProperties = []): object {}
This allows all the following syntax examples to be valid:
$y = clone $x;
$y = clone($x);
$y = \clone($x);
$y = clone($x, []);
$y = clone($x, [
"foo" => $foo,
"bar" => $bar,
]);
$y = clone($x, $array);
By promoting clone to a function, it will also be possible to use it as a callable. E.g., in array_map. Promoting clone to a function also simplifies the implementation considerably.
==== Technical Details ====
* A magic __clone() method will be called before the new properties are assigned.
* Calling __clone() afterward would mean the new properties would already be set and the old ones gone, contrasting existing behavior and thus users' expectations.
* Assignment of new properties happens in iteration order of the array parameter. As a result, should there be an error during the assignment (e.g., because of property hooks), the error will be raised for the first impacted property.
* Property assignments are made just as a regular assignment would be. Meaning all regular PHP rules apply with only the readonly state being "unlocked".
* Visibility rules for property access are enforced. Clone can't be used to modify the internal state of objects from outside.
* Property hooks work as expected. Setters are called during clone calls.
* Dynamic properties respect #[AllowDynamicProperties].
* __set works as expected and is called during clone as it would be during normal assignments.
* The behavior of Lazy Objects is not affected; as before, they will be de-lazyfied and cloned.
A userland implementation of this would be replacing the clone call with the following code:
$value) {
$cloned->{$key} = $value;
}
return $cloned;
The clone keyword (and T_CLONE token) will be kept and it will remain impossible to create a clone function within a namespace. It will also not be possible to disable the clone() function with the ''disable_functions=clone'' INI setting.
==== Design Goals ====
When re-proposing this RFC, one of our goals was to take all previous discussion into account and propose something that is small in scope and cognitive load.
This RFC explicitly rejects any BC impacting syntax choices like the previously proposed with keyword, as we don't feel the scope of the feature warrants any BC impact.
Likewise, all syntax has to exist in PHP already, to not add cognitive load to readers when they come across it. A simple PHP array feels like the most ubiquitous syntax in PHP.
Furthermore, this RFC considers touching how __clone works (e.g. by adding parameters to it) to be a non-goal due to the increased complexity and [[#backward_incompatible_changes|BC considerations]].
==== Examples ====
=== Using clone as a callable ===
object(stdClass)#4 (0) {}
// [1] => object(stdClass)#5 (0) {}
// }
=== Cloning with new properties ===
$newC]);
}
}
$x = new Foo();
// object(Foo)#2 (1) { ["c":"Foo":private]=> int(1) }
var_dump($x->just_clone());
// object(Foo)#2 (1) { ["c":"Foo":private]=> int(5) }
var_dump($x->clone_with(5));
=== Visibility rules ===
Public properties can be changed from the outside during cloning.
int(5) }
var_dump(clone($foo, ["pub" => 5]));
Public readonly properties can be changed during cloning from within the class.
$newPub]);
}
}
$foo = new Foo();
// object(Foo)#2 (1) { ["pub"]=> int(5) }
var_dump($foo->withPub(5));
Public readonly properties need a public(set) to be able to be cloned from outside the class, as the property is protected(set) by default.
This is existing PHP behavior and unchanged by this RFC. Changing the visibility of readonly properties is outside the scope of this RFC, and this example is provided for sake of completeness and to thoroughly explain the existing edge cases.
5]));
// Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Foo::$pub from global scope in ...
Protected and private properties can be changed from within their respective scopes.
$priv]);
}
}
class Bar extends Foo {
public function withProt(int $prot) {
return clone($this, ["prot" => $prot]);
}
}
$foo = new Foo();
// object(Foo)#2 (1) { ["prot":protected]=> uninitialized(int) ["priv":"Foo":private]=> int(10) }
var_dump($foo->withPriv(10));
$bar = new Bar();
// object(Bar)#3 (1) { ["prot":protected]=> int(5) ["priv":"Foo":private]=> uninitialized(int) }
var_dump($bar->withProt(5));
// Fatal error: Uncaught Error: Cannot access protected property Bar::$prot in
clone($bar, ["prot" => 5]);
=== Assignment order and property hooks ===
Property hooks are executed just as they would be with normal setters.
hooked = strtoupper($value);
}
}
}
$x = new Foo();
// object(Foo)#1 (1) { ["hooked"] => string(7) "default" }
var_dump($x);
// object(Foo)#2 (1) { ["hooked"] => string(7) "UPDATED" }
var_dump(clone($x, ["hooked" => 'updated']));
=== Throw order ===
Properties are set in the order they are passed in. The first error/exception raised cancels the rest of the clone operation.
5,
"b" => 'invalid argument',
"c" => 'also invalid'
]);
}
}
$foo = new Foo(1,2,3);
$foo->withInvalidValues();
// Fatal error: Uncaught TypeError: Cannot assign string to property Foo::$b of type int in ...
Combined with property hooks:
3) {
throw new InvalidArgumentException("Rejecting $value");
}
}
},
private int $c {
set (int $value) {
echo "Got $value", PHP_EOL;
}
},
) {}
public function withInvalidValues() {
return clone($this, [
"a" => 5,
"b" => 6,
"c" => 7,
]);
}
}
$foo = new Foo(1,2,3);
$foo->withInvalidValues();
// Got 1
// Got 2
// Got 3
// Got 5
// Fatal error: Uncaught InvalidArgumentException: Rejecting 6 in ...
=== Readonly ===
int(1) }
var_dump(clone($x, ["c" => 2])); // object(Foo)#2 (1) { ["c"]=> int(2) }
// Without the (set) modifier on public, we'd get:
// Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Foo::$c from global scope in example.php:12
// Note that this is how readonly properties work in PHP currently and any change would be outside the scope of this RFC
=== Dynamic Properties ===
Since property assignments are made just as a regular assignment would be, dynamic property creation follows established PHP rules:
int(2) }
var_dump(clone($x, ["propertyThatDoesNotExist" => 2]));
#[AllowDynamicProperties]
class Bar {
}
$y = new Bar();
// object(Bar)#3 (1) { ["propertyThatDoesNotExist"]=> int(2) }
var_dump(clone($y, ["propertyThatDoesNotExist" => 2]));
=== Numeric Arrays ===
Given the function signature clone(object $object, array $withProperties): object {} it follows that numeric parameters derive their key from their implicit position as they would for any other PHP array. For clone this is usually not useful and is documented here for completeness.
// int(1)
// ["1"]=>
// int(2)
// }
Omitting the array keys for most objects will lead to deprecation notices, as shown in the "Dynamic Properties" section.
===== Backward Incompatible Changes =====
No BC break is expected. The new syntax is optional and the old syntax will continue to work as before.
By not touching how __clone() works, we also avoid any BC impact for users of this function.
===== Proposed PHP Version(s) =====
Next PHP 8.x (8.5)
===== RFC Impact =====
==== To SAPIs ====
None
==== To Existing Extensions ====
The current implementation adds a new ''clone_obj_with'' object handler that needs to be implemented to make internal classes compatible with clone with when custom cloning behavior is already implemented with ''clone_obj''. If the default ''clone_obj'' handler is used, clone with will transparently work.
The existing behavior of ''clone_obj'' when no properties are given remains fully compatible. Thus, the only impact is that extensions that are not updated will be incompatible with clone with, but cloning behavior without properties remains compatible for existing PHP programs.
==== To Opcache ====
None
==== New Constants ====
None
==== php.ini Defaults ====
None
===== Open Issues =====
None
===== Unaffected PHP Functionality =====
===== Future Scope =====
* Allow __clone() to inspect the $withProperties array to skip deep-cloning properties that will be overridden.
===== Proposed Voting Choices =====
* Yes
* No
===== Patches and Tests =====
* https://github.com/php/php-src/pull/18747
===== Implementation =====
TBD
===== References =====
* https://wiki.php.net/rfc/clone_with
* https://externals.io/message/120048
===== Rejected Features =====
For the reasons mentioned in the introduction and the Backwards Incompatible Changes section, changes to the magic __clone() method are out of scope for this RFC.
===== Changelog =====
Earlier versions of this RFC proposed a clone method with named-parameters. This was dropped in favor of less complex functions with more familiar syntax and no newly invented concepts not seen anywhere else in php-src. The RFC, the implementation and the documentation efforts needed were significantly reduced by this change.