DEV Community

hbgl
hbgl

Posted on • Edited on • Originally published at hbgl.dev

5

Elegant immutable object pattern in PHP

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
);
Enter fullscreen mode Exit fullscreen mode

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'
);
Enter fullscreen mode Exit fullscreen mode

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',
);
Enter fullscreen mode Exit fullscreen mode

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,
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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'
);
Enter fullscreen mode Exit fullscreen mode

The post Elegant immutable object pattern in PHP appeared first on hbgl.

Do your career a favor. Join DEV. (The website you're on right now)

It takes one minute and it's free.

Get started

Top comments (0)

Cloudinary image

Video API: manage, encode, and optimize for any device, channel or network condition. Deliver branded video experiences in minutes and get deep engagement insights.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay