PHP 8.1: cloning and changing readonly properties
Note: PHP 8.3 adds a built-in way of cloning readonly properties, although it's rather limited in its possibilities. Read more.
In PHP 8.1, readonly properties aren't allowed to be overridden as soon as they are initialized. That also means that cloning an object and changing one of its readonly properties isn't allowed. It's likely that PHP will get some kind of clone with
functionality in the future, but for now we'll have to work around the issue.
Let's imagine a simple DTO class with readonly properties:
class Post { public function __construct( public readonly string $title, public readonly string $author, ) {} }
PHP 8.1 would throw an error when you'd clone a post object and tried to override one of its readonly properties:
$postA = new Post(title: 'a', author: 'Brent'); $postB = clone $postA; $postB->title = 'b'; Error: Cannot modify readonly property Post::$title
The reason why this happens is because the current readonly implementation will only allow a value to be set as long as it's uninitialized. Since we're cloning an object that already had a value assigned to its properties, we cannot override it.
It's very likely PHP will add some kind of mechanism to clone objects and override readonly properties in the future, but with the feature freeze for PHP 8.1 coming up, we can be certain this won't be included for now.
So, at least for PHP 8.1, we'll need a way around this issue. Which is exactly what I did, and why I created a package that you can use as well: https://github.com/spatie/php-cloneable.
Here's how it works. First you download the package using composer, and next use the Spatie\Cloneable\Cloneable
trait in all classes you want to be cloneable:
use Spatie\Cloneable\Cloneable; class Post { use Cloneable; public function __construct( public readonly string $title, public readonly string $author ) {} }
Now our Post
objects will have a with
method that you can use to clone and override properties with:
$postA = new Post(title: 'a', author: 'Brent'); $postB = $postA->with(title: 'b'); $postC = $postA->with(title: 'c', author: 'Freek');
There are of course a few caveats:
- this package will skip calling the constructor when cloning an object, meaning any logic in the constructor won't be executed; and
- the
with
method will be a shallow clone, meaning that nested objects aren't cloned as well.
I imagine this package being useful for simple data-transfer and value objects; which are exactly the types of objects that readonly properties were designed for to start with.
For my use cases, this implementation will suffice. And since I believe in opinion-driven design, I'm also not interested in added more functionality to it: this package solves one specific problem, and that's good enough.