Immutable Objects in PHP

Original author: Mark Ragazzo, edited by Alexander Makarov and Vladimir Chub
  • Transfer
In this short article, we will look at what immutable objects are and why we should use them. Immutable are objects whose state has remained constant from the moment of their creation. Usually such objects are very simple. Surely you are already familiar with enum types or primitives like DateTimeImmutable. Below we will see that if you make simple objects immutable, this will help to avoid certain errors and save a lot of time.

When implementing immutable objects, you must:

  • Declare a class finalso that it cannot be overridden when adding methods that change the internal state.
  • Declare properties as privateso that again they cannot be changed.
  • Avoid setters and use the constructor to set parameters.
  • Do not store references to mutable objects or collections. If you store a collection inside an immutable object, then it must also be immutable.
  • Check that if you need to modify an immutable object, you made a copy of it, rather than reusing the existing one.

If you change the object in one place, then unwanted side effects that can be difficult to debug can appear in another. This can happen anywhere: in third-party libraries, in language structures, etc. Using immutable objects will help to avoid such troubles.

So, what are the advantages of correctly implemented immutable objects:

  • The state of a program becomes more predictable because fewer objects change their own state.
  • Due to the fact that situations with shared references become impossible, debugging is simplified.
  • Immutable objects are conveniently used to create parallel executable programs (this article is not considered).

Note: immutability can still be broken with the help of “reflections”, serialization / deserialization, binding of anonymous functions or magic methods. However, all this is quite difficult to implement and is unlikely to be used by accident.

Let's move on to the example of an immutable object:

city = (string)$city;
        $this->house = (string)$house;
        $this->flat = (string)$flat;
    }
    public function getCity()
    {
        return $this->city;
    }
    public function getHouse()
    {
        return $this->house;
    }
    public function getFlat()
    {
        return $this->flat;
    }
}

Once created, this object no longer changes state, so it can be considered immutable.

Example


Let's now analyze the situation with the transfer of money to accounts in which the lack of immutability leads to erroneous results. We have a class Moneythat represents a certain amount of money.

amount;
    }
    public function add($amount)
    {
        $this->amount += $amount;
        return $this;
    }
}

We use it as follows:

add($userAmount->getAmount() * 0.03);
/**
 * Получаем с карты Марка для последующего перевода 2 доллара + 3% комиссии
 */
$markCard->withdraw($processedAmount);
/**
 * Отправляем Алексу 2 доллара
 */
$alexCard->deposit($userAmount);

Note: the float type here is used only for simplicity. In real life, to perform the operation with the necessary accuracy, you will need to use the bcmath extension or some other vendor library.

Should be fine. But due to the fact that the class is Moneychangeable, instead of two dollars, Alex will receive 2 dollars and 6 cents (commission 3%). The reason is that $userAmount, and $processedAmountrefer to the same object. In this case, it is recommended to use an immutable object.

Instead of modifying an existing object, you must create a new one or make a copy of the existing object. Let's change the above code by adding another object to it:

amount;
    }
}


val() * 3 / 100;
$processedAmount = Money::USD($userAmount->getAmount() + $commission);
$markCard->withdraw($processedAmount);
$alexCard->deposit($userAmount);

This works well for simple objects, but in case of complex initialization it is better to start by copying an existing object:

amount;
    }
    public function add($amount)
    {
        return new self($this->amount + $amount, $this->currency);
    }
}

It is used in exactly the same way:

add($userAmount->val() * 0.03);
/**
 * Получаем с карты Марка для последующего перевода 2 доллара + 3% комиссии
 */
$markCard->withdraw($processedAmount);
/**
 * Отправляем Алексу 2 доллара
 */
$alexCard->deposit($userAmount);

This time, Alex will receive his two dollars without a commission, and Mark and this amount and commission will be correctly written off.

Random volatility


When implementing mutable objects, programmers can make mistakes that make objects become mutable. It is very important to know and understand this.

Object link internal leak


We have a mutable class, and we want the immutable object to use it.

y = $y;
    }
}
class Immutable
{
    protected $x;
    public function __construct($x)
    {
        $this->x = $x;
    }
    public function getX()
    {
        return $this->x;
    }
}

An immutable class has only getters, and the only property is assigned by the constructor. At first glance, everything is in order, right? Now let's use this:

getX();
var_dump(md5(serialize($immutable))); // f48ac85e653586b6a972251a85dd6268

The object remains the same, the state has not changed. Perfectly!

Now let's play a little with X:

getX()->setY(5);
var_dump(md5(serialize($immutable))); // 8d390a0505c85aea084c8c0026c1621e

The state of an immutable object has changed, so that it actually turned out to be mutable, although everything said otherwise. This happened because during the implementation the rule “do not store references to mutable objects”, given at the beginning of this article, was ignored. Remember: immutable objects should only contain immutable data or objects.

Collections


Using collections is common. But what if, instead of constructing an immutable object with another object, we construct it with a collection of objects?

First, let's implement the collection:

elements = $elements;
    }
    public function add($element)
    {
        $this->elements[] = $element;   
    }
    public function get($key)
    {
        return isset($this->elements[$key]) ? $this->elements[$key] : null ;
    }
}

Now use this:

getX();
var_dump(md5(serialize($immutable))); // 9d095d565a96740e175ae07f1192930f
$immutable->getX()->get(0)->setY(5);
var_dump(md5(serialize($immutable))); // 803b801abfa2a9882073eed4efe72fa0

As we already know, it is better not to keep mutable objects inside immutable. Therefore, we replace mutable objects with scalars.

getX();
var_dump(md5(serialize($immutable))); // 24f1de7dc42cfa14ff46239b0274d54d
$immutable->getX()->add(10);
var_dump(md5(serialize($immutable))); // 70c0a32d7c82a9f52f9f2b2731fdbd7f

Since our collection provides a method for adding new elements, we can indirectly change the state of an immutable object. So when working with a collection inside an immutable object, make sure that it is not mutable. For example, make sure that it contains only immutable data. And that there are no methods that add new elements, remove them or otherwise change the state of the collection.

Inheritance


Another common situation is inheritance. We know what we need:

  • Use getters only
  • create instances through the constructor,
  • inside objects of immutable objects store only immutable data.

Let's modify the class Immutableso that it Immutableaccepts only -objects.

x = $x;
    }
    public function getX()
    {
        return $this->x;
    }
}

It looks good ... until someone extends your class:

x = $x;
    }
}


getX()->getX()))); // c52903b4f0d531b34390c281c400abad
var_dump(md5(serialize($immutable->getX()->getX()))); // 6c0538892dc1010ba9b7458622c2d21d
var_dump(md5(serialize($immutable->getX()->getX()))); // ef2c2964dbc2f378bd4802813756fa7d
var_dump(md5(serialize($immutable->getX()->getX()))); // 143ecd4d85771ee134409fd62490f295

Everything went wrong again. This is why immutable objects must be declared as finalso that they cannot be expanded.

Conclusion


We learned what an immutable object is, where it can be useful and what rules must be observed when implementing it:

  • Declare a class finalso that it cannot be overridden when adding methods that change the internal state.
  • Declare properties as privateso that again they cannot be changed.
  • Avoid setters and use the constructor to set parameters.
  • Do not store references to mutable objects or collections. If you store a collection inside an immutable object, then it must also be immutable.
  • Check that if you need to modify an immutable object, you made a copy of it, rather than reusing the existing one.

Also popular now: