As many of you know, immutability is an extremely useful concept that makes code more predictable and generally easier to understand. Case in point, in PHP we have the mutable DateTime
as well as DateTimeImmutable
, the latter being recommended over the former.
And why is that?
Well, being an immutable object means that it can be passed around fearlessly without worrying about someone mutating it. No more guessing whether you should make a defensive copy or not. Plus, the API of DateTimeImmutable
is not any worse than that of DateTime
, so you get all of the benefits basically for free.
But it does not stop with DateTimeImmutable
. We were always able to write our own immutable class types in PHP, and with the recently introduced readonly properties (PHP 8.1) and readonly classes (PHP 8.2) it is now easier than ever.
Let me show you a quick example of an immutable Address
class:
<?php
readonly class Address {
public function __construct(
public string $name,
public string $line1,
public string $line2,
public string $city,
public string $state,
public string $zipCode,
public ?string $phone,
) {}
}
$myAddress = new Address(
'John Doe',
'4711 Main St',
'Apt 4',
'Anytown',
'CA',
'90210',
null
);
How to update properties
Having immutable objects is nice and all but in the real world people do change their addresses. In the context of immutable objects, we “update” properties by creating new versions of existing objects. So instead of mutating the existing object, we create a new object that is a copy except for the properties we want to change.
Here is what it would look like if we wanted to add a phone number to the address:
<?php
$myAddress = new Address(
'John Doe',
'4711 Main St',
'Apt 4',
'Anytown',
'CA',
'90210',
null
);
$myNewAddress = new Address(
$myAddress->name,
$myAddress->line1,
$myAddress->line2,
$myAddress->city,
$myAddress->state,
$myAddress->zipCode,
'(555) 555-1234'
);
While the code is easy to understand, it is also quite verbose. It took 9 lines to update a single property since all properties had to be passed into the constructor. This problem gets even worse the more properties a class has. It is not hard to imagine that this would be a deal breaker for some, and a reason to fall back to mutable objects.
Fortunately, we can do a lot better. Let me show you an alternative method that updates a property in only a single line of code:
<?php
$myNewAddress = $myAddress->with(phone: '(555) 555-1234');
// We can also change two properties.
$myOtherAddress = $myAddress->with(
city: 'Sometown',
phone: '(555) 555-6789',
);
Pretty neat, right? To understand what is going on, let’s take a look at the implementation.
<?php
enum Unchanged {
case Value;
}
readonly class Address {
public function __construct(
public string $name,
public string $line1,
public string $line2,
public string $city,
public string $state,
public string $zipCode,
public ?string $phone,
) {}
public function with(
string|Unchanged $name = Unchanged::Value,
string|Unchanged $line1 = Unchanged::Value,
string|Unchanged $line2 = Unchanged::Value,
string|Unchanged $city = Unchanged::Value,
string|Unchanged $state = Unchanged::Value,
string|Unchanged $zipCode = Unchanged::Value,
string|null|Unchanged $phone = Unchanged::Value,
) {
return new self(
$name !== Unchanged::Value ? $name : $this->name,
$line1 !== Unchanged::Value ? $line1 : $this->line1,
$line2 !== Unchanged::Value ? $line2 : $this->line2,
$city !== Unchanged::Value ? $city : $this->city,
$state !== Unchanged::Value ? $state : $this->state,
$zipCode !== Unchanged::Value ? $zipCode : $this->zipCode,
$phone !== Unchanged::Value ? $phone : $this->phone,
);
}
}
We added the new with()
method whose parameters directly correspond to the class properties except that each of them has the special default value Unchanged::Value
. This default value allows us to differentiate between the properties we want to change and the ones we want to keep.
All that’s left is to call with()
using named arguments, passing only the specific properties we want to change. Et voilĂ , we made immutable objects a lot more ergonomic to work with.
<?php
$myNewAddress = $myAddress->with(phone: '(555) 555-1234');
// vs.
$myNewAddress = new Address(
$myAddress->name,
$myAddress->line1,
$myAddress->line2,
$myAddress->city,
$myAddress->state,
$myAddress->zipCode,
'(555) 555-1234'
);
One thought on “Elegant immutable object pattern in PHP”