The case for immutability
Tags: php, programming
Published: 2022-04-29 19:05:21

Some common questions I get on pull requests are "what's with those with-methods?" and "why do you use DateTimeImmutable?". The reason is I am a fan of _immutability_. I became a fan of immutability the hard way; By introducing bugs caused by (unintentional) mutability.

## What is immutability
When an object is immutable its state can not be modified after the object has been created. Concrete this means that an immutable object does not have any _writable_ properties nor any _setters_ to update the properties.

For clarification; The most simple implementation of a _mutable_ object:

```php
final class MutableObject
{
    public string $type = '';
}
```

A more elaborate implementation could have a constructor and a setter. Using a constructor and a setter allows for validation on the value for both initialization and for mutation:

```php
final class MutableObject
{
    private string $type;
    
    public function __construct(string $type)
    {
        $this->setType($type);
    }
    
    public function setType(string $type): void
    {
        if (empty($type)) {
            throw new InvalidArgumentException('Type cannot be empty');
        }

        $this->type = $type;
    }
    
    public function getType(): string
    {
        return $this->type;
    }
}
```

## Why make an object immutable
In short; To guarantee the validity of an object.

In PHP scalar values - integer, float, string and boolean are _passed by value_ by default. That means that in a function or method where I pass an integer as argument we can assign the value of that integer without any side effects on the code that made the call: 

```php
function doSomething(int $number): void
{
    $number += 5;
}

$number = 12345;
echo $number . PHP_EOL;

doSomething($number);
echo $number . PHP_EOL;
```

This above example will output:

```
12345
12345
```

Objects and arrays[^1] are by default _passed by reference_. That means that any changes I apply on an object inside a function or method that was passed as an argument will have side effects on the case that made the call:

```php
function doSomething(DateTime $date): void
{
    $date->add(new DateInterval('P1D'));
}

$date = new DateTime("2022-04-26");
echo $date->format('Y-m-d') . PHP_EOL;

doSomething($date);
echo $date->format('Y-m-d') . PHP_EOL;
```

This above example will  output:

```
2022-04-26
2022-04-27
```

The contents of `$date` has been mutated. And there is no way to prevent this in PHP. Apart from making sure your objects can not be mutated. With an immutable object you can be sure that another piece of code does not (accidentally) change the object causing unexpected side effects. Let's assume another example - a scenario I actually encountered myself:

```php
$today = new DateTime(); // this could be injected from a clock

$openingHours = $this->openingHoursService->getOpeningHoursForUpcomingDays($store, $today, 7);

if ($this->isCurrentlyClosed($today, $openingHours)) {
    // try to find an alternative that is currently open
    $currentlyOpenStores = $this->openingHoursService->getCurrentlyOpen($today);
    // ...
}
```

After releasing this change it took a few days for one of the stakeholders to start complaining about wrong information being shown. We were unable to reproduce this until a colleague decided to set `$today` to the moment the stakeholder reported the issue. Now we saw the wrong information as well! Stepping through the code we discovered that properties of `$today` were changing after calling the opening hours service. We had a look at the implementation and found that it queried the database as follows:

```sql
select
    open, closed
from
    opening_hours
where 
    store = {store_id}
    and open >= {$today}
    and open < {$today->add(new DateInterval("P${days}D"))}
```

Because `$today` is an instance of `DateTime` calling the [`add()`](https://www.php.net/manual/en/datetime.add.php) method on it actually updated the internal value of `$today`. 

We can now get into a discussion about this implementation. If the developer that implemented this method had used the date manipulation features of the database this issue would not have existed. More important is that _unexpectedly_ an unrelated piece of code changed "my object". This is exactly the scenario where immutability can help; If we were to pass an immutable object we can be absolutely certain that no matter how `getOpeningHoursForUpcomingDays()` is implemented the object that we pass to it will never change.

The solution for this specific scenario was to replace the `DateTime` with `DateTimeImmutable`. It required a change in signature for the service we called, but from that moment on our bug was fixed.

## How to implement an immutable object
The immutable equivalent of `MutableObject` could look something like this:

```php
final class ImmutableObject
{
    private string $type;
    
    public function __construct(string $type)
    {
        if (empty($type)) {
            throw new InvalidArgumentException('Type cannot be empty');
        }

        $this->type = $type;
    }
    
    public function getType(): string
    {
        return $this->type;
    }
}
```

So what about those "with-methods" mentioned earlier. Ideally all properties are to be part of the constructor. This can lead to some readability difficulties when working with a large amount of properties and when some properties are optional. Another scenario is when you want to change one or more values after instantiation of the object. The convention we have been using - and that is used in PSR's as well - is to use methods that start with `with`:

```php
final class ImmutableObject
{
    private string $type;
    private ?string $label = null;
    
    public function __construct(string $type, ?string $label = null)
    {
        if (empty($type)) {
            throw new InvalidArgumentException('Type cannot be empty');
        }

        $this->type = $type;
        $this->label = $label;
    }
    
    public function getType(): string
    {
        return $this->type;
    }
    
    public function withLabel(string $label): static
    {
        return new self($this->type, $label);
    }
}
```

Another implementation uses `clone`. This implementation is useful when an object had a high number of properties or when the validity of your object requires a combination of arguments to be specified[^2]:

```php
final class ImmutableObject
{
    private string $type;
    private ?string $label = null;
    
    public function __construct(string $type)
    {
        if (empty($type)) {
            throw new InvalidArgumentException('Type cannot be empty');
        }

        $this->type = $type;
    }
    
    public function getType(): string
    {
        return $this->type;
    }
    
    public function withLabel(string $label): static
    {
        $clone = clone $this;
        $clone->label = $label;
        
        return $clone;
    }
}
```

Instantiating these objects becomes a bit more verbose, but will almost resemble prose:

```php
$object = (new ImmutableObject('my-type'))
    ->withLabel('My type');
```

In the examples shown so far I have opted for verbosity. With the syntactic sugar that is introduced in PHP 8 and PHP 8.1 we can shorten object definitions drastically:

```php
final class ImmutableObject
{
    public function __construct(
        public readonly string $type
    ) {}
}
```

This example will make the property available for usage by other objects. If you prefer to use methods for retrieval of data all you need to do is make the property `private` and introduce a `getter` similar to the examples above.

## Should every object be immutable?
Short answer: no. In my opinion writing code is like being in traffic; You need to be predictable. I think most data containers would benefit from immutability. There are however some scenarios where a mutable object makes more sense as some objects are expected to maintain state. For example a form abstraction; A form contains objects that represent the fields and these fields have a value. This value can be pre-populated, but will be updated based on the info that is submitted. In that scenario it makes sense that the form instance has some form of state.

## tl/dr;
Mutable objects can cause bugs that might be difficult to find. By making object immutable you have the guarantee that you are in control of your objects and their state. Making objects in immutable requires only a relative small amount of code. 

[^1]: Arrays are technically passed by reference, but because most operations on an array creates a new array instance this is not very apparent. Calling methods that change the internal pointer of an array - like `reset()` and `next()` - _do_ have effects on the array instance and can therefore "leak". 

[^2]: PHP 8 has introduced [named arguments](https://stitcher.io/blog/php-8-named-arguments). This makes working with a large number of arguments easier. The downsides of this are that your argument names have now become part of your public facing API and you will have to add more complex validation rules when arguments can only be set in combination with other arguments.