Skip to content

RFC: Clone with v2 #18747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

RFC: Clone with v2 #18747

wants to merge 10 commits into from

Conversation

TimWolla
Copy link
Member

@TimWolla TimWolla commented Jun 3, 2025

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

see TimWolla#6 for a preliminary review.

@TimWolla TimWolla marked this pull request as ready for review July 7, 2025 10:27
@TimWolla TimWolla requested review from iluuu1994 and nielsdos July 7, 2025 10:28
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I did not (yet) carefully look at all tests.


if (EXPECTED(!EG(exception)) && zend_hash_num_elements(properties) > 0) {
/* Unlock readonly properties once more. */
if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce) && old_object->ce->clone) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to unlock all properties again? After all, multiple writes to to readonly properties are forbidden within __clone. This problem can still occur with something like:

class C {
    public function __construct(
        private readonly string $prop,
    ) {}
    public string $a { set { $this->prop = $value; } }
    public string $b { set { $this->prop = $value; } }
}

var_dump(clone(new C('foo'), [
    'a' => 'a',
    'b' => 'b',
]));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless, this can be skipped if !old_object->ce->clone as nothing happened between unlocking and this path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to unlock all properties again?

I found it easiest to just unlock them again, instead of trying to find the property fields corresponding to the “with” properties.

Regardless, this can be skipped if !old_object->ce->clone as nothing happened between unlocking and this path.

This condition is already there, or did I misunderstand?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it easiest to just unlock them again, instead of trying to find the property fields corresponding to the “with” properties.

My statement wasn't very precise. I was more wondering whether we need to unlock them at all. Namely, isn't it at least somewhat expected a property written to by __clone shouldn't also be overwritten again? It wasn't clearly specified how this would be handled in the RFC from what I can see. Regardless, I understand why you're doing it so probably just disregard.

This condition is already there, or did I misunderstand?

No, I just missed it. Apologies.


ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object)
{
return zend_objects_clone_obj_with(old_object, old_object->ce, &zend_empty_array);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It seems like scope is unused when the properties array is empty. Maybe it's better to pass NULL here? Same in zend_objects_clone_members().

{
zend_object *new_object;

/* Compatibility with code that only overrides clone_obj. */
if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this problematic for overridden clone_obj that reuse zend_objects_clone_obj? In that case old_object->handlers->clone_obj != zend_objects_clone_obj would hit, and we'd recurse infinitely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There are none in php-src and generally speaking it seems unlikely that there are any, since when there is a need to overwrite clone there likely also is a need to overwrite create (which explicitly isn't supported):

/* assume that create isn't overwritten, so when clone depends on the

  • overwritten one then it must itself be overwritten */

Most of the custom clone handlers just use zend_objects_clone_members().


This PR tries to minimize the disruption for existing code, but if there is an external extension that does what you suggest, then it's easy enough for the author to adjust it to add an explicit clone_with handler.

Copy link
Member

@iluuu1994 iluuu1994 Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could do the inverse: Keep clone_obj as is, call it whenever properties are not provided or when the array is empty. When it isn't, clone_obj_with is invoked, which can call clone_obj to clone the object, and adjust the properties afterwards. This way, an extension could simply continue to provide clone_obj and the property part would just work. This approach seems more logical to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which can call clone_obj to clone the object, and adjust the properties afterwards

This is likely dangerous, since some internal classes might expect their (readonly) properties not to change, which leads to some C structures related to a property not being properly adjusted. Random\Randomizer is currently not cloneable at all, but it stores both a public readonly \Random\Engine $engine property exposed to userland and also the raw “engine pointer” for more efficient access.

The implementation also prefers the clone_with handler, since I would like to see the old clone_obj hook to go away in the future to remove the duplication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely dangerous, since some internal classes might expect their (readonly) properties not to change

Can this already be broken with __clone in a user-declared sub-class? I can't see cases other than enums preventing the declaration of __clone. The property would need to be private(set) to prevent this.


class Clazz {
public string $hooked = 'default' {
set (string $value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
set (string $value) {
set {

NIt: Let's stay consistent with the code style in existing tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants