PHPnews.io

Cloning and changing readonly properties in PHP 8.1

Written by Stitcher.io / Original link on Jul. 6, 2021

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.

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 follow me on Twitter or subscribe to my newsletter: Email Subscribe

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.

stitcher.io

« Some Light Chapter Clean Up - Xdebug Update: June 2021 »