Hi Le mer. 4 juin 2025 à 15:33, Tim Düsterhus <t...@bastelstu.be> a écrit :
> 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. > This argument is totally symmetric: currently calling __clone is both the first and the last thing that happens. There can be implementations that rely on properties being unchanged after the call to __clone, eg: function __clone() { $this->foo = clone $this->foo; $this->fooId = spl_object_id($this->foo); } The example is silly but that's to highlight the symmetry. We spotted that passing $this to __clone would allow inspecting the changes and implementing deep-cloning protection, but only if calling __clone happens after. My concern is that as is, the RFC would block this possible future improvement - because then, it would be a non-BC behavior change. > > 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)`. > Using ArrayObject was for a quick example. A stricter class can be implemented though but the deep-cloning issue would remain. "readonly" is also totally optional. I could have used a more trivial example: class Response { public ArrayObject $headers; public function __construct(array $headers) { $this->headers = new ArrayObject($headers); } public function __clone() { $this->headers = clone $this->headers; } } > > 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. > I was not suggesting that made any difference. This was about highlighting the deep-cloning issue. > > 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. > Fair, thanks for addressing this. > -------- > > > 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. > Nice, I got lost in possibilities and missed that, thanks. > > 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). > Let's leave this as such, I'm fine with a wait-n-see approach to that concern. > > 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 > I think you've got the issue: deep-cloning. It's the only real thing I'm talking about and it's missing from the RFC - well, except in the future scope now, but that's a bit light to raise proper awareness about the design choice IMHO; Also, the change doesn't tell about "or the original object", I was surprised to see that difference between your email and the RFC update. Any reason for that? This relates to my call-ordering concern above. Cheers, Nicolas