Elegant immutable object pattern in PHP
Tags: php, webdev
Published: 2024-01-27 22:13:38

As many of you know, [immutability](https://en.wikipedia.org/wiki/Immutable_object) 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](https://www.php.net/manual/en/class.datetime) 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](https://php.watch/versions/8.1/readonly) (PHP 8.1) and [readonly classes](https://php.watch/versions/8.2/readonly-classes) (PHP 8.2) it is now easier than ever.

Let me show you a quick example of an immutable `Address` class:

```php
<?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
<?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
<?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
<?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](https://php.watch/versions/8.0/named-parameters), passing only the specific properties we want to change. Et voilà, we made immutable objects a lot more ergonomic to work with.

```php
<?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'
);
```
The post [Elegant immutable object pattern in PHP](https://hbgl.dev/elegant-immutable-object-pattern-in-php/) appeared first on hbgl.