This is an old revision of the document!
PHP RFC: Clone with
- Date: 2022-10-24
- Author: Máté Kocsis kocsismate@php.net
- Status: Under Discussion
- Target Version: PHP 8.3
- Implementation: https://github.com/php/php-src/pull/9497
Introduction
With the advent of standards promoting “quasi-immutable” objects, like PSR-7, “wither” methods became increasingly widely used. This practice is the “de facto” standard to modify the object state without altering the currently existing references to an object. A “wither” method copy-pasted from Diactoros basically looks like this:
class Response implements ResponseInterface { public int $statusCode; public string $reasonPhrase; // ... public function withStatus($code, $reasonPhrase = ''): Response { $new = clone $this; $new->statusCode = $code; $new->reasonPhrase = $reasonPhrase; return $new; } // ... }
By using this approach, one can effectively implement immutable objects. As the “quasi” above indicates, these objects are not entirely immutable, since workarounds exist for modifying their state. As of PHP 8.1 though, PHP RFC: Readonly properties 2.0 provides a foundational building block for preventing these workarounds: by adding the readonly
modifier to a property, one can make sure that once initialized, it cannot be modified anymore. This would allow PSR-7 implementations to properly enforce their immutability.
Unfortunately, PSR-7 implementations cannot make use of readonly properties yet, unless they always instantiate a new class instance rather than using cloning as described above. This cumbersome workaround would be required because readonly properties have too strict constraints: they are readonly at the property level, while in order to be useful in practice, they should be readonly only at the object level. The former means that after initialization, a property is unmodifiable throughout its entire life span, while the latter means that it is unmodifiable during the life cycle of an object. The main difference between the two approaches is whether a property is modifiable after the object is cloned.
Since PHP RFC: Readonly amendments was (partially) accepted for PHP 8.3, readonly properties became modifiable during the execution of the __clone()
magic method, allowing deep cloning of readonly properties. This was the first step towards achieving object-level readonliness, however the general problem of supporting safe modification of readonly properties was not solved yet.
Proposal
The current proposal aims to add support for a new language construct called “clone with” by extending the clone
operator, that would make it possible to write “wither” methods for any kind of instance properties (declared/dynamic, typed/untyped, readonly/non-readonly) with less code.
Using “clone with”, the above example can be rewritten the following way:
class Response implements ResponseInterface { public readonly int $statusCode; public readonly string $reasonPhrase; // ... public function withStatus($code, $reasonPhrase = ''): Response { return clone $this with { statusCode: $code, reasonPhrase: $reasonPhrase }; } // ... }
This syntax expresses that the Response::$statusCode
and Response::$reasonPhrase
properties are modified right after $this
is cloned, leaving the original object intact. This way, it becomes possible to atomically clone and modify readonly properties, even multiple times:
$response = new Response(200); $response->withStatus(201)->withStatus(202);
Constraints
Furthermore, “clone with” respects all visibility rules and type constraints, just like how regular assignments do:
class Foo { public int $a; protected int $b; private $c; public readonly int $d; } class Bar extends Foo { public function withC() { return clone $this with {c: 1}; } public function withD() { return clone $this with {d: 1}; } } $bar = new Bar(); clone $bar with {a: "abc"};// Exception due to type mismatch clone $bar with {b: 1}; // Exception due to accessing a protected property from the global scope $bar->withC(); // Exception due to accessing a private property from a child class $bar->withD(); // Exception due to assigning a readonly property from a child class clone $bar with {e: []}; // Deprecation notice due to assigning to a dynamic property
Property name expressions
So far, all “clone with” examples introduced in the current RFC used fixed property names which were referenced as identifiers followed by a colon (:
), while the values to be assigned were expressions. This syntax is a very straightforward and concise way for the most basic case when “clone with” operates on a known property.
However, in some cases it would be useful to reference property names as expressions, e.g. when one needs to use “clone with” in a foreach loop where the index is the property name and the loop variable is the value to be assigned. This is also possible using a slightly more verbose syntax:
class Foo { private $a; private $b; private $c; /** * @param array<string, mixed> $properties */ public function withProperties(array $properties) { $self = clone $this; foreach ($properties as $name => $value) { $self = clone $this with {$name => $value}; } return $self; } } $foo = new Foo(); $foo->withProperties(["a" => 1, "b" => 2, "c" => 3]);
This time, both side of the assignment is an expression, separated by =>
. Not only variables, but any kind of expressions strictly evaluating to a string type are possible to use:
$object = new stdClass(); $object = clone $object with {"foo" => 1}; // the property name is a literal $object = clone $object with {strtolower("FO") . "o" => 1}; // the property name is an expression $object = clone $object with {PROPERTY_NAME => 1}; // the property name is a named constant
The last example demonstrates well why it's necessary to use different separator tokens (=>
vs. :
) for the two cases: otherwise it wouldn't be clear if one references the property name indirectly via the value of a global constant or directly as an identifier.
Interaction with the __clone magic method
If an object is being “cloned with” which has a __clone()
magic method then the execution order is the following:
__clone()
is executed- The “clone with” assignments are executed from left to right. Any properties - including the ones modified by the
__clone()
method - are modifiable again.
Alternatives
Nicolas Grekas proposed an alternative idea which could also make it possible to reinitialize readonly properties:
class Abc { private readonly $foo; public clone function withFoo($newFoo): static { $this->foo = $newFoo; return $this; } }
According to which, the clone
method modifier would implicitly clone $this
, therefore any assignments performed on it would act on the new instance only -- effectively preventing the modification of a property on an existing instance.
Even though this is a pretty clever way to solve the original issue, it has some serious drawbacks:
- One cannot control whether
$this
should really be cloned: e.g. if a property should only be modified based on certain conditions (e.g. validation), the object would potentially be cloned in vain, resulting in a performance loss. - The behavior of a “clone method” is counterintuitive for beginners as it needs some experience to be able to determine what
$this
actually refers to. - Sometimes one also needs access to the original instance, but doing so would only be possible via workarounds (e.g. by introducing
$that
or a similar construct).
class LinkedObject { public function __construct( private readonly LinkedObject $next, private readonly int $number ) { $this->number = 1; } public clone function next(): LinkedObject { $this->number++; $that->next = $this; // a hypothetical $that variable is needed here return $this; } }
The same code using “clone with”:
class LinkedObject { public function __construct( private readonly LinkedObject $next, private readonly int $number ) { $this->number = 1; } public function next(): LinkedObject { $self = clone $this with {number: $this->number + 1} $this->next = $self; return $self; } }
The biggest advantage of Nicolas' proposal over “clone with” is that it could be made part of the interface contract whether a method doesn't modify the object state. So the PSR-7 ResponseInterface
could look like the following:
interface ResponseInterface extends MessageInterface { // .. public clone function withStatus($code, $reasonPhrase = ''); // .. }
The position of this RFC is that cloning the current object and making sure that its state is not modified should be done separately as opposed to what the clone
modifier does: “clone with” deals with the former aspect, while a new readonly
method modifier could handle the latter in the future. This separation is advantageous because the two features could be used on their own. That's one of the main reasons why this RFC prefers and implements “clone with”.
Reflection
The proposal doesn't have impact for reflection.
Backward Incompatible Changes
If the proposal gets accepted, with
becomes a reserved keyword.
Vote
The vote requires 2/3 majority.