[PHP-DEV] [RFC] Clone with v2

Hi

Am 2025-05-21 17:27, schrieb Theodore Brown:

Combining named-parameter `array()` syntax with clone taking a array as
the second parameter would allow for the following, which might combine
the best of both worlds?

    clone($obj, array(foo: 1, bar: "baz", object: "this is not blocked"));

I really like this idea! If it can work without any function call overhead,
it would enable more ergonomic array creation not only for `clone()` but
also in many other common scenarios.

Yes, the implementation in the PR comes without any function call overhead. The logic to support the named parameters is purely implemented in the parser. The actual function implementation is provided to “round off” the implementation and because it was super simple to add.

Best regards
Tim Düsterhus

On Wed, May 21, 2025, at 9:13 AM, Tim Düsterhus wrote:

Hi

Am 2025-05-19 12:48, schrieb Volker Dusch:

We're still looking for feedback on the ...variadic approach to the
Syntax:
PHP: rfc:clone_with_v2, as we only got one
reply so far on the topic.

I was hoping for some additional opinions here before adding my own, but
since this does not appear to happen, adding my personal opinion on this
matter now:

*Some* property name being completely incompatible with “clone with” (no
matter how the first parameter is going to be called) is a limitation
that should not exist, it feels like a feature that is broken by design
and I think I really would hate it if the documentation of this RFC
would need a “Caution: It is not possible to reassign a property called
'$object', due to a parameter name conflict”.

I completely disagree here. The __ prefix is sufficient to solve the issue. A double underscore prefix has been understood to mean "internal to PHP, magic here" for at least 20 years. The chances of someone having a property called $__object are virtually nil, and if they do, they're already treading on naming that's reserved for PHP's use anyway so if it breaks, it's their own fault.

Adjusting the signature to `clone(object $object, array
$withProperties)` would not have this problem and I don't consider the
additional verbosity of an array literal to be a problem. Static
analysis tools already understand array shapes and would need
adjustments either way to understand the semantics.

From an implementation PoV a regular array parameter would also be
simpler, since the implementation would be able to simply pass along the
input array, whereas the “variadic” syntax needs special handling to
combine positional parameters with named parameters into a single array
that is then used in the cloning process.

Syntax-wise there might also be a middle-ground. Similarly to how this
RFC turns `clone()` into a function, the `array()` syntax also looks
like a function call and would naturally extend to named parameters.
While mixing positional and named parameters would probably get complex,
allowing purely named parameters would trivially be possible (without
any function call overhead). It would also allow using the
first-class-callable syntax with `array(...)`, something I would liked
to have in the past. A proof of concept PR is at:

Make `array()` a function by TimWolla · Pull Request #18613 · php/php-src · GitHub

Combining named-parameter `array()` syntax with clone taking a array as
the second parameter would allow for the following, which might combine
the best of both worlds?

     clone($obj, array(foo: 1, bar: "baz", object: "this is not
blocked"));

I completely disagree here as well. I actively dislike using an array here, as I do find quoting all the keys to be cumbersome. And I don't think we should go with a worse syntax in the hopes of it being improved later, when there may well be other unforeseen challenges there. (Eg, I have no idea if bare array keys would cause issues with constants.) Moreover, we have 15 years of training and coding standards and automation tools that say to never use array(), always . Reversing that would be far more disruptive for the ecosystem, even if it turns out to be not too disruptive to the code.

I strongly prefer the current syntax, and if the RFC changed to require an array I would strongly consider voting No on those grounds alone.

--Larry Garfield

On Wed, May 21, 2025 at 23:27 Larry Garfield wrote:

On Wed, May 21, 2025, at 9:13 AM, Tim Düsterhus wrote:

Am 2025-05-19 12:48, schrieb Volker Dusch:

We're still looking for feedback on the ...variadic approach to the
Syntax:
PHP: rfc:clone_with_v2, as we only got one
reply so far on the topic.

...
*Some* property name being completely incompatible with “clone with” (no
matter how the first parameter is going to be called) is a limitation
that should not exist, it feels like a feature that is broken by design
and I think I really would hate it if the documentation of this RFC
would need a “Caution: It is not possible to reassign a property called
'$object', due to a parameter name conflict”.

I completely disagree here. The __ prefix is sufficient to solve the issue.
A double underscore prefix has been understood to mean "internal to PHP, magic
here" for at least 20 years. The chances of someone having a property called
$__object are virtually nil, and if they do, they're already treading on naming
that's reserved for PHP's use anyway so if it breaks, it's their own fault.

Is the __ prefix part of the current proposal? Currently the RFC examples have
the parameter name as simply `$object`. Even if the parameter is renamed to
`$__object`, there would still have to be a warning in the docs that it isn't
possible to use the function to set a property named `__object`, which I agree
with Tim is a limitation that should not exist.

For people who want to consistently use named arguments for all parameters,
(rather than all parameters except the first one) the code would look like:

    $obj = clone(
        __object: $this,
        foo: 'bar',
        object: $baz,
    );

Is this API really better than with a simple array parameter?

    $obj = clone($this, [
        'foo' => 'bar',
        'object' => $baz,
    ]);

Using an array results in fewer lines of code, and the distinction between
the object being cloned and the properties being set is much clearer.

I get that it may be annoying to quote array keys, but to me this is not
as bad as the confusing variadic API. In the future PHP could get a simpler
array syntax like `[foo: 'bar']`, which would benefit far more functions than
only `clone()`.

Kind regards,
Theodore

Version 1.1 Update: Array syntax over named arguments.

Thank you everyone for the discussion and for improving this RFC. I’m very happy with the updates we made thanks to your feedback on and off list.

The main idea of this RFC was to have as little of a footprint as possible and make it feel natural in PHP. I got carried away a bit during writing this, and by using named arguments, introduced something that isn’t used in php-src provided functions, had edge cases that required documentation, and all in all, didn’t feel natural in PHP but like an inconsistency. PHP uses arrays to pass lists everywhere else, so changing this here feels like too much scope for this little change. I’m very happy this was caught and raised in the discussions. Thank you.

If there is desire for named-parameter-as-an-array syntax this should be standalone RFC with more scope than a single function.

We’ve updated the RFC and the implementation, removing the “Open Issue” listed before, as this change resolved all of them.

···

Volker Dusch
Head of Engineering

Tideways GmbH
Königswinterer Str. 116
53227 Bonn
https://tideways.io/imprint

Sitz der Gesellschaft: Bonn
Geschäftsführer: Benjamin Außenhofer (geb. Eberlei)
Registergericht: Amtsgericht Bonn, HRB 22127

Hi Volker,

Thanks for the update.

Le lun. 26 mai 2025 à 16:05, Volker Dusch <volker@tideways-gmbh.com> a écrit :

Version 1.1 Update: Array syntax over named arguments.

Thank you everyone for the discussion and for improving this RFC. I’m very happy with the updates we made thanks to your feedback on and off list.

The main idea of this RFC was to have as little of a footprint as possible and make it feel natural in PHP. I got carried away a bit during writing this, and by using named arguments, introduced something that isn’t used in php-src provided functions, had edge cases that required documentation, and all in all, didn’t feel natural in PHP but like an inconsistency. PHP uses arrays to pass lists everywhere else, so changing this here feels like too much scope for this little change. I’m very happy this was caught and raised in the discussions. Thank you.

If there is desire for named-parameter-as-an-array syntax this should be standalone RFC with more scope than a single function.

We’ve updated the RFC and the implementation, removing the “Open Issue” listed before, as this change resolved all of them.

To me, it would have made more sense to still make “clone” an operator but I think we can all get used to the array style, so if the majority thinks arrays should be the way to go, so be it.

I think the RFC is missing a few bits to be complete:

  • making “clone” a function means suddenly a “use clone;” or a “\clone” is going to be needed to not get a perf hit, isn’t it? But since $y = clone $x; is still going to be valid, how will this be handled? The RFC could give some more hints on the topic.

  • writing code with a wide range of supported PHP versions means we’ll start using construct such as:
    if (PHP_VERSION_ID>=80500) {
    $b = ‘clone’($a, […]); // note ‘clone’ is a string here, so that the line is valid syntax on PHP <= 8.4
    else {
    // another code path for PHP 8.4
    }
    That’s of course because “use clone”, “\clone” or “clone($foo)” is invalid PHP syntax currently.
    It could make sense to tell about this and show an example like this one in the RFC.

  • what if one gives a mangled property in the array? clone($foo, [“\0Foo\0bar” => 123])
    It could be useful to write something about this case and/or at least have a test case that shows how this should behave.

  • To me, the main technical drawback of the RFC is that it builds on mandatory deep-cloning, which means wasted CPU/mem resources by design. This has to be mentioned in the RFC IMHO so that voters can make an informed decision.

About this last point, it’s the one that makes me undecided about my voting decision on the RFC.
I shared my concern and a solution in another thread, where I also address the BC concern that is mentioned in the RFC. Since this concern has been addressed, I think this should be considered more closely. As is, that’s a big omission to me - recognizing and addressing the issue with deep-cloning.

Cheers,
Nicolas

Hi

Clarifying on the technical questions.

Am 2025-05-26 16:37, schrieb Nicolas Grekas:

I think the RFC is missing a few bits to be complete:

- making "clone" a function means suddenly a "use clone;" or a "\clone" is
going to be needed to not get a perf hit, isn't it? But since $y = clone
$x; is still going to be valid, how will this be handled? The RFC could
give some more hints on the topic.

The implementation of making clone a function matches that of making `exit()` a function in PHP 8.4 (PHP: rfc:exit-as-function). Specifically, `clone` will remain a keyword and any use of a bare `clone` will compile to a fully-qualified call to the clone function, as if you would have written `\clone()`. For the same reason it will remain impossible to define a `clone()` function within a namespace. It will also be disallowed to use `disable_functions=clone` (the same as with exit and die).

That `clone()` will become a function should be considered an implementation detail. You might start seeing a `clone()` frame in your stack traces, within `assert(false && clone($foo))` the `\clone()` call will be fully-qualified in the error message output and error messages might slightly change (e.g. when passing a non-object to clone). Other than that, it should not be noticeable. Perhaps even the ZEND_CLONE opcode will remain when using clone with a single-parameter (this is similarly to the other optimization of “special functions”, such as count() or sprintf()).

I have made the following changes to the RFC that should hopefully clarify things: PHP: rfc:clone_with_v2

- writing code with a wide range of supported PHP versions means we'll
start using construct such as:
if (PHP_VERSION_ID>=80500) {
   $b = 'clone'($a, [...]); // note 'clone' is a string here, so that the
line is valid syntax on PHP <= 8.4
else {
   // another code path for PHP 8.4
}
That's of course because "use clone", "\clone" or "clone($foo)" is invalid
PHP syntax currently.

Only `use function clone;` is currently invalid syntax (and it will remain invalid syntax, similarly to `use function exit;`). `clone($a)` currently is a perfectly valid cloning statement (with redundant parentheses) and `\clone($a)` is a valid call to a function called `clone` that does not exist: Online PHP editor | output for VB3j1

But as I said above, it is not necessary to change any existing code. The RFC is clear on that: There are no backwards incompatible changes regarding the usage. When finalizing the implementation, I'll make sure to run some benchmarks to determine whether or not keeping the OPcode is worth it for the “non-with” case, or whether making clone a “frameless” function would bring any benefit. But making sure it runs fast is the job of the engine, not the job of the PHP developer and generally speaking performance characteristics can change even without RFCs.

It could make sense to tell about this and show an example like this one in
the RFC.

Given that your question appears to be based on a misunderstanding, I believe adding such an example to the RFC is not useful.

- what if one gives a mangled property in the array? clone($foo,
["\0Foo\0bar" => 123])
It could be useful to write something about this case and/or at least have
a test case that shows how this should behave.

As the RFC specifies, clone-with behaves exactly like regular property assignments. This means your example behaves like `$foo->{"\0Foo\0bar"} = 123;` and thus will throw “Error: Cannot access property starting with "\0"”. I have added a test to the implementation [1]. Adding that to the RFC text itself is probably not useful to the average reader, since this is not something they will encounter.

Best regards
Tim Düsterhus

[1] Clone with by TimWolla · Pull Request #6 · TimWolla/php-src · GitHub

On Mon, May 26, 2025 at 08:03 Volker Dusch wrote:

Version 1.1 Update: Array syntax over named arguments.

Thank you everyone for the discussion and for improving this RFC.
I'm very happy with the updates we made thanks to your feedback on and off list.

The main idea of this RFC was to have as little of a footprint as possible and
make it feel natural in PHP. I got carried away a bit during writing this, and
by using named arguments, introduced something that isn't used in php-src
provided functions, had edge cases that required documentation, and all in all,
didn't feel natural in PHP but like an inconsistency. PHP uses arrays to pass
lists everywhere else, so changing this here feels like too much scope for this
little change. I'm very happy this was caught and raised in the discussions.
Thank you.

If there is desire for named-parameter-as-an-array syntax this should be
standalone RFC with more scope than a single function.

We've updated the RFC and the implementation, removing the "Open Issue" listed
before, as this change resolved all of them.

- PHP: rfc:clone_with_v2
- Clone with by TimWolla · Pull Request #6 · TimWolla/php-src · GitHub

Thank you Volker and Tim for your work on this RFC. It will be a great quality-
of-life improvement for the language, and I look forward to voting in favor.

Theodore

Hi Nicolas,

Thank you for the great email and for thinking this through. Getting input from the folks that maybe will use this feature the most is very valuable, and I appreciate that you’re taking the time to do this early.

  • To me, the main technical drawback of the RFC is that it builds on mandatory deep-cloning, which means wasted CPU/mem resources by design. This has to be mentioned in the RFC IMHO so that voters can make an informed decision.

About this last point, it’s the one that makes me undecided about my voting decision on the RFC.
I shared my concern and a solution in another thread, where I also address the BC concern that is mentioned in the RFC. Since this concern has been addressed, I think this should be considered more closely. As is, that’s a big omission to me - recognizing and addressing the issue with deep-cloning.

Quoting from your other email:

Allow to skip deep-cloning of already updated properties (that’s a significant drawback of the current proposal - deep cloning before setting is a potential perf/mem hog built into the engine) : guarding deep-cloning with a strict comparison would be enough.

To make sure we’re on the same page: This would only come into effect if both a __clone method exists and clone($thing, $withProperties) is used.

  • We decided to __clone before updating properties to avoid BC issues.
  • For readonly classes, given their dependencies are likely to be readonly as well, they don’t need to be deep cloned in most cases?
  • Therefore, I think combining __clone and clone-with will be a rare occasion, but all performance issues can be avoided by only using one of the two approaches in classes where that matters.
  • This RFC leaves future modifications to __clone open, so we could discuss a follow-up of passing the properties to __clone if we have a strategy to handle the BC issues, but for now, I see this as adding too much complexity for a very limited set of use cases that already are solved by the people that have them.

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.

  • writing code with a wide range of supported PHP versions means we’ll start using construct such as:
    if (PHP_VERSION_ID>=80500) {
    $b = ‘clone’($a, […]); // note ‘clone’ is a string here, so that the line is valid syntax on PHP <= 8.4
    else {
    // another code path for PHP 8.4
    }

Tim has already addressed the \namespace fallback and that this doesn’t apply here, and that no performance penalty is introduced into any existing codebases.

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.

Kind Regards,
Volker

···

Volker Dusch
Head of Engineering

Tideways GmbH
Königswinterer Str. 116
53227 Bonn
https://tideways.io/imprint

Sitz der Gesellschaft: Bonn
Geschäftsführer: Benjamin Außenhofer (geb. Eberlei)
Registergericht: Amtsgericht Bonn, HRB 22127

Hi everyone,

As there was no additional feedback for the last 5 days, and we feel the RFC is in a good place, we intend to start voting on Wednesday if there are no new concerns raised.

Thank you again!
Volker

···

Volker Dusch
Head of Engineering

Tideways GmbH
Königswinterer Str. 116
53227 Bonn
https://tideways.io/imprint

Sitz der Gesellschaft: Bonn
Geschäftsführer: Benjamin Außenhofer (geb. Eberlei)
Registergericht: Amtsgericht Bonn, HRB 22127

Volker Dusch
Head of Engineering

Tideways GmbH
Königswinterer Str. 116
53227 Bonn
https://tideways.io/imprint

Sitz der Gesellschaft: Bonn
Geschäftsführer: Benjamin Außenhofer (geb. Eberlei)
Registergericht: Amtsgericht Bonn, HRB 22127

Hi Volker,

Sorry for the delay in answering, it’s been a long week-end AFK on my side.

Le mer. 28 mai 2025 à 16:52, Volker Dusch <volker@tideways-gmbh.com> a écrit :

Hi Nicolas,

Thank you for the great email and for thinking this through. Getting input from the folks that maybe will use this feature the most is very valuable, and I appreciate that you’re taking the time to do this early.

On Mon, May 26, 2025 at 4:37 PM Nicolas Grekas <nicolas.grekas+php@gmail.com> wrote:

  • To me, the main technical drawback of the RFC is that it builds on mandatory deep-cloning, which means wasted CPU/mem resources by design. This has to be mentioned in the RFC IMHO so that voters can make an informed decision.

About this last point, it’s the one that makes me undecided about my voting decision on the RFC.
I shared my concern and a solution in another thread, where I also address the BC concern that is mentioned in the RFC. Since this concern has been addressed, I think this should be considered more closely. As is, that’s a big omission to me - recognizing and addressing the issue with deep-cloning.

Quoting from your other email:

Allow to skip deep-cloning of already updated properties (that’s a significant drawback of the current proposal - deep cloning before setting is a potential perf/mem hog built into the engine) : guarding deep-cloning with a strict comparison would be enough.

To make sure we’re on the same page: This would only come into effect if both a __clone method exists and clone($thing, $withProperties) is used.

Correct.

  • 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.

  • For readonly classes, given their dependencies are likely to be readonly as well, they don’t need to be deep cloned in most cases?
  • Therefore, I think combining __clone and clone-with will be a rare occasion, but all performance issues can be avoided by only using one of the two approaches in classes where that matters.

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 don’t think this is/will be/should be a rare design, nor do I think this should be ignored in the design of the proposed RFC.

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.

  • This RFC leaves future modifications to __clone open, so we could discuss a follow-up of passing the properties to __clone if we have a strategy to handle the BC issues, but for now, I see this as adding too much complexity for a very limited set of use cases that already are solved by the people that have them.

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”…

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.

  • writing code with a wide range of supported PHP versions means we’ll start using construct such as:
    if (PHP_VERSION_ID>=80500) {
    $b = ‘clone’($a, […]); // note ‘clone’ is a string here, so that the line is valid syntax on PHP <= 8.4
    else {
    // another code path for PHP 8.4
    }

Tim has already addressed the \namespace fallback and that this doesn’t apply here, and that no performance penalty is introduced into any existing codebases.

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.

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 it to the RFC of course - at least there’s this thread for history and this could be documented later on somewhere else.
Note that the new syntax being the only way to swap the headers while cloning means that all similarly-designed classes will have to live by the deep-cloning issue. As a corollary, authors that will want to avoid the deep-cloning issue will be forced to skip such designs. And that’s a shame because that design looks desirable to me (and authors might realize the issue after-the-fact, which is even worse…)

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

Nicolas

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: PHP: rfc:clone_with_v2

Best regards
Tim Düsterhus

Hi

Le mer. 4 juin 2025 à 15:33, Tim Düsterhus <tim@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