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.

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

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