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:

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.

Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what's happening on this blog, you can subscribe to my mailing list: send an email to brendt@stitcher.io, and I'll add you to the list.