📜 ⬆️ ⬇️

Unchangeable objects in PHP

In this short article, we will look at what immutable objects are and why we should use them. Objects whose state remains constant from the moment of their creation are called immutable. Usually such objects are very simple. You are probably already familiar with enum types or primitives like DateTimeImmutable . Below, we will see that if you make simple objects immutable, this will help avoid certain errors and save a lot of time.

When implementing immutable objects, you must:


If in one place to change the object, then in the other unwanted side effects may appear, which are difficult to debug. This can happen anywhere: in third-party libraries, in language structures, etc. The use of immutable objects will avoid such troubles.

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

Note: immutability can still be broken using 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 chance.

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

 <?php final class Address { private $city; private $house; private $flat; public function __construct($city, $house, $flat) { $this->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 does not change the state, so it can be considered immutable.

Example


Let us now analyze the situation with the transfer of money in the accounts, in which the absence of immutability leads to erroneous results. We have a class of Money , which represents a certain amount of money.

 <?php class Money { private $amount; public function getAmount() { return $this->amount; } public function add($amount) { $this->amount += $amount; return $this; } } 

We use it as follows:

 <?php $userAmount = Money::USD(2); /** *     2 .   3%, *       . */ $processedAmount = $userAmount->add($userAmount->getAmount() * 0.03); /** *        2  + 3%  */ $markCard->withdraw($processedAmount); /** *   2  */ $alexCard->deposit($userAmount); 

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

Should be fine. But due to the fact that the Money class is changeable, instead of two dollars, Alex will receive $ 2 and 6 cents (3% commission). The reason is that $userAmount and $processedAmount refer 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 an existing object. Let's change the code above by adding another object to it:

 <?php final class Money { private $amount; public function getAmount() { return $this->amount; } } 


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

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

 <?php final class Money { private $amount; public function getAmount() { return $this->amount; } public function add($amount) { return new self($this->amount + $amount, $this->currency); } } 

It is used in the same way:

 <?php $userAmount = Money::USD(2); /** *     2 .   3%, *       . */ $processedAmount = $userAmount->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 will correctly write off this amount and commission.

Random variability


When implementing modifiable objects, programmers may make mistakes that cause objects to become variable. It is very important to know and understand.

Internal reference link leak


We have a mutable class, and we want it to be used by an immutable object.

 <?php class MutableX { protected $y; public function setY($y) { $this->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 a single property is assigned by the constructor. At first glance, everything is in order, right? Now let's use this:

 <?php $immutable = new Immutable(new MutableX()); var_dump(md5(serialize($immutable))); // f48ac85e653586b6a972251a85dd6268 $immutable->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:

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

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

Collections


The use of collections is a common phenomenon. 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:

 <?php class Collection { protected $elements = []; public function __construct(array $elements) { $this->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:

 <?php $immutable = new Immutable(new Collection([new XMutable(), new XMutable()])); var_dump(md5(serialize($immutable))); // 9d095d565a96740e175ae07f1192930f $immutable->getX(); var_dump(md5(serialize($immutable))); // 9d095d565a96740e175ae07f1192930f $immutable->getX()->get(0)->setY(5); var_dump(md5(serialize($immutable))); // 803b801abfa2a9882073eed4efe72fa0 

As we already know, it’s better not to keep mutable objects inside unchangeable. Therefore, we replace replaceable objects with scalars.

 <?php $immutable = new Immutable(new Collection([1, 2])); var_dump(md5(serialize($immutable))); // 24f1de7dc42cfa14ff46239b0274d54d $immutable->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 itself is not changeable. 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 related to inheritance. We know what you need:


Let's modify the Immutable class to accept only Immutable objects.

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

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

 <?php class Mutant extends Immutable { public function __construct() { } public function getX() { return rand(1, 1000000); } public function setX($x) { $this->x = $x; } } 


 <?php $mutant = new Mutant(); $immutable = new Immutable($mutant); var_dump(md5(serialize($immutable->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. That is why immutable objects must be declared final so that they cannot be expanded.

Conclusion


We have learned what an immutable object is, where it can be useful and what rules need to be observed when it is implemented:

Source: https://habr.com/ru/post/301004/


All Articles