# RFC: Clone with v2

- Status: Implemented (PHP 8.5)
- Authors: Volker Dusch (volker@tideways-gmbh.com), Tim Düsterhus (tim@tideways-gmbh.com)
- Date: 2025-03-30
- Version: 1.1
- Target: PHP 8.5
- Implementation: https://github.com/php/php-src/commit/ca49a7bec2a0a8d77bfa4b6d375ca0ffa4edc5ee, https://github.com/php/php-src/commit/c9249e2d3aa401bda5d9a3071e86e0594807ed00, https://github.com/php/php-src/commit/7f4076bae0cf91c5a24f4f908525e739e6bca8fb
- Original: https://wiki.php.net/rfc/clone_with_v2
- Page: https://externals.io/rfc/clone_with_v2
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Implement clone-with as outlined in the RFC? (closed)

Yes: 16 · No: 4

Voters: alec (No), asgrim (Yes), beberlei (Yes), crell (No), daniels (Yes), derick (Yes), edorian (Yes), galvao (Yes), geekcom (Yes), kalle (No), kguest (Yes), kocsismate (Yes), ocramius (Yes), ramsey (No), seld (Yes), sergey (Yes), thekid (Yes), theodorejb (Yes), timwolla (Yes), weierophinney (Yes)

## Discussions on the mailing list

- [\[RFC\] \[Discussion\] Clone with](https://externals.io/message/120048.md) — started on 2023-04-17, 55 messages
- [\[RFC\] Clone with v2](https://externals.io/message/127353.md) — started on 2025-05-14, 52 messages
- [\[VOTE\] Clone with v2](https://externals.io/message/127588.md) — started on 2025-06-04, 15 messages

---

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

```php
<?php
 
final readonly class Response {
    public function __construct(
        public int $statusCode,
        public string $reasonPhrase,
        // ...
    ) {}
 
    public function withStatus($code, $reasonPhrase = ''): Response
    {
        // Only works if all properties are assignable via __construct
        $values = get_object_vars($this);
        $values['statusCode'] = $code;
        $values['reasonPhrase'] = $reasonPhrase;
        return new self(
            ...$values
        );
    }
}
```

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.

```php
    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](https://externals.io/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.

```php
function clone(object $object, array $withProperties = []): object {}
```

This allows all the following syntax examples to be valid:

```php
$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:

```php
<?php
 
$cloned = clone $object;
foreach ($withProperties as $key => $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 [BC considerations](#backward_incompatible_changes).

### Examples

#### Using clone as a callable

```php
<?php
 
$x = [new stdClass, new stdClass];
 
var_dump(array_map(clone(...), $x));
array_map('clone', $x); // Works as well
 
// array(2) {
//  [0] => object(stdClass)#4 (0) {}
//  [1] => object(stdClass)#5 (0) {}
// }
```

#### Cloning with new properties

```php
<?php
 
class Foo {
 
    public function __construct(
        private readonly int $c = 1,
    ) {}
 
    public function just_clone() {
        return clone $this;
    }
 
    public function clone_with($newC) {
        return clone($this, ["c" => $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.

```php
<?php
 
class Foo {
    public int $pub = 1;
}
 
$foo = new Foo();
// object(Foo)#2 (1) { ["pub"]=> int(5) }
var_dump(clone($foo, ["pub" => 5]));
```

Public `readonly` properties can be changed during cloning from within the class.

```php
<?php
 
class Foo {
    public readonly int $pub;
 
    public function withPub($newPub) {
        return clone($this, ["pub" => $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.

```php
<?php
 
class Foo {
    public readonly int $pub;
}
 
$foo = new Foo();
var_dump(clone($foo, ["pub" => 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.

```php
<?php
 
class Foo {
    protected readonly int $prot;
    private readonly int $priv;
 
    public function withPriv(int $priv) {
        return clone($this, ["priv" => $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.

```php
<?php
 
class Foo {
    public string $hooked = 'default' {
        set (string $value) {
            $this->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.

```php
<?php
 
class Foo {
 
    public function __construct(
        private int $a,
        private int $b,
        private int $c,
    ) {}
 
    public function withInvalidValues() {
        return clone($this, [
            "a" => 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:

```php
<?php
 
class Foo {
 
    public function __construct(
        private int $a {
            set (int $value) {
                echo "Got $value", PHP_EOL;
            }
        },
        private int $b {
            set (int $value) {	
                if ($value > 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

```php
<?php
 
class Foo {
    public function __construct(
        public(set) readonly int $c,
    ) {}
}
 
$x = new Foo(1);
 
var_dump($x); // object(Foo)#1 (1) { ["c"]=> 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:

```php
<?php
 
class Foo {
}
 
 
$x = new Foo();
// Deprecated: Creation of dynamic property Foo::$propertyThatDoesNotExist is deprecated in ...
// object(Foo)#2 (1) { ["propertyThatDoesNotExist"]=> 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.

```php
<?php
 
var_dump(
    clone(new stdClass, [1, 2])
);
 
// object(stdClass)#2 (2) {
//   ["0"]=>
//   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

**Vote: Implement clone-with as outlined in the RFC?** — Yes: 16 · No: 4 (closed)

## Patches and Tests

- <https://github.com/php/php-src/pull/18747>

## Implementation

- <https://github.com/php/php-src/commit/ca49a7bec2a0a8d77bfa4b6d375ca0ffa4edc5ee>
- <https://github.com/php/php-src/commit/c9249e2d3aa401bda5d9a3071e86e0594807ed00>
- <https://github.com/php/php-src/commit/7f4076bae0cf91c5a24f4f908525e739e6bca8fb>

## References

- [https://wiki.php.net/rfc/clone\_with](https://externals.io/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.
