Hi

Am 2025-06-03 16:24, schrieb Nicolas Grekas:
- We decided to __clone before updating properties to avoid BC issues.


Not sure which BC issues you've in mind, especially as that's a new feature.
As I see it, before or after wouldn't change anything as far as __clone
implementations are concerned.

While clone-with is a new feature, it is automatically supported on every class. Currently calling `__clone()` is the first thing that happens after cloning the object and the implementation of `__clone()` might rely on the properties being unchanged from the original object. If the reassignment of properties would happen before the call to `__clone()` using clone-with with classes that have such a `__clone()` implementation might result in subtle and hard to debug issues.

That's a very common mistake about readonly: it's also very useful for
mutable classes. Here is an example that the Symfony Request/Response
objects might follow one day:

class Response
{
    public function __construct(
        public string $content = '',
        public *readonly* ArrayObject $headers = new ArrayObject(),
    ) {
    }

    public function __clone() {
        $this->headers = clone $this->headers;
    }
}

$r1 = new Response();
$r2 = clone $r1;
$r3 = clone($r1, ['headers' => new ArrayObject(['foo' => 'bar'])]);

Note how making "headers" readonly binds the headers to its owner object. That's very useful since it forbids swapping the headers object by another
one in response objects.

I might lack the Symfony background, but it's not clear to me how preserving the object-identity, but not its contents is important here. I'd like to note, though:

- Given that ArrayObject allows for interior mutability, you could just use `$r3->headers->exchangeArray(['foo' => 'bar']);`. - Exchanging the entire `ArrayObject` can only happen from within the class itself, since `$headers` is implicitly `protected(set)`. - You don't actually bind the `$headers` ArrayObject to the owner object, since the ArrayObject might be referenced by multiple Response objects, since passing it in the constructor is legal. The same would be true if it would be `public(set)`.

The issue I highlighted previously is that __clone will "clone
$this->headers" even if the resulting value will be immediately trashed on
the line that creates $r3. This can cascade to nested clones of course
depending on what is being cloned. That's where I see a waste of CPU/mem
"by-design", which is unavoidable at the moment.

The same would be true if you reassign the property after the successful cloning.

About the BC aspect I already shared a proposal that should avoid it. About the "very limited set of use cases", that's simply not true: people will start using the new syntax when consuming third-party classes, and authors
of those classes will be "Sorry Outta Luck" when their users will start
complaining about the perf hog I described, with no other options than
telling "don't use that syntax, that perfhog is built-in"...

I don't see that happening, because it would mean that folks suddenly start modifying `public(set) readonly` properties during cloning. In other cases, the RFC does not enable anything new. For non-readonly properties, the user is already able to reassign (see above) and for non-public(set) properties, the class itself controls both `__clone()` and the property itself and thus can take the use-cases into account. On other words, what Volker already said here:

So no situation will be worse than it was before, while some use-cases are enabled now and some performance critical code will probably stay the same
as it is now.

--------

So my personal suggestion would to be for code that is doing custom things
during cloning and support PHP pre-clone-with as is and only refactor once
8.5 is the minimum requirement, in places where it improves the code.


I didn't answer Tim but he missed that in my example there is a second
argument passed to clone(), which makes it a parse error currently.

In that case you can use:

    if (PHP_VERSION_ID >= 80500) {
        $b = \clone($a, ['foo' => 'bar']);
    }

`\clone` is syntactically valid in PHP 8.0 or higher and a reference to the function `clone` in the global namespace. Incidentally this kind of cross-version compatibility is enabled by making `clone()` a function.

About the second aspect: if a class is designed with the API I gave as
example above (that Response class), then the new syntax is going to be the only way to swap the headers while cloning. That means there will be cases where my snippet is going to be useful. I'm fine if you don't want to add

If this syntax is the only way to swap headers while cloning, how would the branch for PHP 8.4 or lower look like? As Volker suggested, you can only switch to clone-with when PHP 8.5 is your minimum version, since you would need the “legacy way of doing things” for older PHP versions (and that one would still remain valid even with the changes this RFC proposes).

So what remains on my side is giving enough care to the deep-cloning case -
see my previous messages of course.

When building a solution it's important to understand the problem in depth and given that we don't currently understand the problem (or even see a problem), we are not in a position to build a solution. Importantly though the RFC leaves sufficient design space for a “Future Scope” solution (e.g. one that passes the $withProperties array to `__clone()` or the original object - which would enable your WeakMap use-case but is orthogonal to clone-with). I have just added that to the “Future Scope” section: https://wiki.php.net/rfc/clone_with_v2?do=diff&rev2%5B0%5D=1748959452&rev2%5B1%5D=1749034411&difftype=sidebyside

Best regards
Tim Düsterhus

Reply via email to