The Tears of a Clone

On the face of it, the new readonly properties in PHP 8.1 seem really useful for Value Objects and Data Transfer Objects: both should be immutable; setting property values should always be handled in the constructor, so no setters; and with readonly properties, we no longer need to code boilerplate getter methods; so we only need to code a comparison method for value objects. Between constructor property promotion introduced in PHP 8, and readonly properties introduced in PHP 8.1, we can reduce the code (skipping the comparison for brevity) for defining a Money class from


class Money {
    protected int $value;
    protected Currency $currency;

    public function __construct(int $value, Currency $currency) 
    {
        $this->value = $value;
        $this->currency = $currency;
    }

    public function getValue(): int {
        return $this->value;
    }

    public function getCurrency(): Currency {
        return $this->currency;
    }
}

to a much more simplistic


class Money {
    public function __construct(
        public readonly int $value,
        public readonly Currency $currency
    ) {}
}

and then we directly access the value and currency of the object externally as object properties, rather than needing to use the getter methods of the original version.

The new version of our Money class looks so much cleaner: we’ve reduced the cognitive overhead, and we’ve eliminated a lot of boilerplate code – readonly properties FTW, right?

But used like this, readonly properties create a problem if we need to clone the Money object, with a modified value. As an example, let’s extend our old version of the Money class to create a couple of new classes so that we can make a Payment that gives us a new Balance:


class Payment extends Money {
}

class Balance extends Money {
    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->currency->name));
        }
        
        $newBalance = clone $this;
        $newBalance->value = $this->value + $payment->value;

        return $newBalance;
    }
}

> Note that we should probably create a deep clone in this case, creating a new instance of Currency as well, but we’ll look at deep clones in a while. For the moment, I want to keep things simple.

This use of clone is a common approach when working with Value Objects: calling addPayment() shouldn’t change the existing Balance object, because it’s immutable, so we return a new instance of a Balance object containing the new value. And because the clone is an instance of the same class, we can directly reference the properties within the makePayment() method without needing to use the getters.

We’d use this new functionality to create our Balance and Payment objects, then we execute the makePayment() method, which returns a new Balance object, leaving the original instance unchanged:


$balance = new Balance(120, new Currency('GBP'));
$payment = new Payment(30, new Currency('GBP'));

$newBalance = $balance->makePayment($payment);

And that code would work perfectly if we were still using the old Money class with protected/private properties.

However, if we try to clone the new readonly property version of our Balance object, and update the value property on the clone in the same way, we’ll get an error, because the value property has already been initialised during the cloning process.


Cannot modify readonly property Money::$value

The object has been successfully cloned as an identical clone; but we can’t modify any of those readonly properties in the clone because they are readonly.

So suddenly readonly properties look a lot less useful because we can’t use clone with classes that use them such as Value Objects and Data Transfer Objects.


It’s an unfortunate oversight that I hope might be rectified in some way in a future PHP release, perhaps some additional magic in the __clone() magic method, or possibly Property Accessors if an RFC for that is ever accepted; but there are a couple of alternative approaches that can be used instead of clone to work round the issue.

Spatie.be have published a Cloneable Trait that adds a with() method to any class that uses it. While Spatie work mainly within the Laravel ecosystem, this small Trait can be used with any framework, or vanilla PHP. Cloneable uses Reflection to build the new clone, populating modified properties from the list provided as arguments.


class Money {
    use Cloneable;

    public function __construct(
        public readonly int $value,
        public readonly Currency $currency
    ) {}
}

class Payment extends Money {
}

class Balance extends Money {
    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->currency->name));
        }
        
        $newBalance = $this->with(value: $this->value + $payment->value);

        return $newBalance;
    }
}

And this will achieve exactly what we want.

I’d strongly recommend using Spatie.be’s Cloneable Trait; but it does have some limitations.

It will only create a shallow clone, so if we wanted to create a deep clone of our Money object, we’d need to clone our Currency as well, which also has readonly properties, so we’re potentially making the code a bit more complex if we need deep clones.

It also only modifies the properties in the scope of a single class – note that we have to apply the Cloneable Trait against the Money class rather than the Balance class in the above example, because that’s where the properties are actually defined – which can become an issue if we have additional readonly properties defined at different levels within an inheritance tree. Suppose that we want an additional $lastTransactionDateTime property against our Balance object:


class Balance extends Money {
    use Cloneable;

    public readonly DateTimeInterface $lastTransactionDateTime;

    public function __construct(
        int $value,
        Currency $currency,
        DateTimeInterface $lastTransactionDateTime = new DateTimeImmutable(timezone: new DateTimezone('UTC'))
    ) {
        parent::__construct($value, $currency);
        $this->lastTransactionDateTime = $lastTransactionDateTime;
    }

    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->currency->name));
        }
        
        $newBalance = $this->with(
            value: $this->value + $payment->value,
            lastTransactionDateTime: new DateTimeImmutable(timezone: new DateTimezone('UTC'))
        );

        return $newBalance;
    }
}

We might expect this to work, but instead we get


Cannot initialize readonly property Money::$value from scope Balance

The Cloneable Trait doesn’t walk the inheritance tree: We can’t set the Balance properties from the scope of Money, or the Money properties from the scope of Balance; even if we’re applying the Cloneable Trait for both the Money and Balance classes.

That being said, Value Objects and DTOs are often a single level, with no inheritance. But a better approach to our Money/Balance problem would have been to use Composition instead of Inheritance, and to have a Money object as a property of the Balance class.


class Money {
    use Cloneable;

    public function __construct(
        public readonly int $value,
        public readonly Currency $currency
    ) {}
}

class Balance {
    use Cloneable;

    public function __construct(
        public readonly Money $money,
        public readonly DateTimeInterface $lastTransactionDateTime = new DateTimeImmutable(timezone: new DateTimezone('UTC'))
    ) {}

    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->money->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->money->currency->name));
        }
        
        $newBalance = $this->with(
            money: $this->money->with(value: $this->money->value + $payment->value),
            lastTransactionDateTime: new DateTimeImmutable(timezone: new DateTimezone('UTC'))
        );

        return $newBalance;
    }
}


$balance = new Balance(new Money(120, new Currency('GBP')));
$payment = new Payment(30, new Currency('GBP'));

$newBalance = $balance->makePayment($payment);

We need to do a deep clone here to ensure that the money property in our Balance object is also cloned as a new Money object with the correct value. At just two levels of Composition, the code isn’t too ugly or hard to read, although it is an additional level of complexity for setting the new Money value.

Another strong argument for the use of Composition rather than Inheritance.


An alternative approach, if all the properties are public and set through the constructor, and are named matching the constructor’s arguments, is to cast the object to an array and then construct a new instance passing that array as constructor arguments using the spread operator.


class Balance extends Money {
    public readonly DateTimeInterface $lastTransactionDateTime;

    public function __construct(
        int $value,
        Currency $currency,
        DateTimeInterface $lastTransactionDateTime = new DateTimeImmutable(timezone: new DateTimezone('UTC'))
    ) {
        parent::__construct($value, $currency);
        $this->lastTransactionDateTime = $lastTransactionDateTime;
    }

    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->currency->name));
        }
        
        $balanceProperties = (array) $this;
        $balanceProperties['value'] = $this->value + $payment->value;
        $balanceProperties['lastTransactionDateTime'] = new DateTimeImmutable(timezone: new DateTimezone('UTC'));

        $newBalance = new self(...$balanceProperties);

        return $newBalance;
    }
}

When we cast the object to an array, PHP creates an associative array using the property names as keys; and since PHP 8.1 supports using the spread operator with associative arrays, and those keys match the named arguments for the constructor, it will create a new instance of the object with the same values. In order to modify the values that we want changed, we do so against the array before passing it to the constructor. And this approach will work regardless of inheritance level: as long as all the properties are public, and all set through the constructor.
So unlike Cloneable, we don’t need to worry about the level of inheritance, because the assignment of values to properties is all handled by the object constructor.

We can take the same approach even if we’re favouring Composition; although (as with Cloneable) we have to do that manually within our own makePayment() code.


class Balance {
    public function __construct(
        public readonly Money $money,
        public readonly DateTimeInterface $lastTransactionDateTime = new DateTimeImmutable(timezone: new DateTimezone('UTC'))
    ) {}

    public function makePayment(Payment $payment): Balance {
        if ($payment->currency->code !== $this->money->currency->code) {
            throw new Exception(sprintf('Payment must be in %s', $this->money->currency->name));
        }
        
        $objectProperties = (array) $this;
        $objectProperties['money'] = new Money(value: $this->money->value + $payment->value, currency: $this->money->currency);
        $objectProperties['lastTransactionDateTime'] = new DateTimeImmutable(timezone: new DateTimezone('UTC'));

        $newBalance = new Balance(...$objectProperties);

        return $newBalance;
    }
}

We just need to ensure that nested objects that we want to modify are also instantiated with their new values when we set the new values in the array, and assign those newly instantiated nested object as the array value.


So while on the surface readonly properties seem really useful, there are some problems with using them for Value objects where we would normally use clone to create a new version of the object. These problems aren’t insurmountable, there are solutions; but neither Cloneable, nor casting to an array and creating a new instance from that array are perfect.

In either case, I do believe that the benefits of readonly properties outweigh the drawbacks; but we need to be aware of their limitations, and understand how to work with them. Hopefully this article has helped provide some of that understanding.

This entry was posted in PHP and tagged , , , . Bookmark the permalink.

1 Response to The Tears of a Clone

  1. Pingback: ReadOnly Gotchas – A few more limitations with PHP 8.1 ReadOnly Properties | Mark Baker's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s