tl;dr: Immutability in PHP is most practical when the object properties are scalars or nulls. Using streams, objects, or arrays as properties makes it very difficult, sometimes impossible, to preserve immutablity.
One of the tactics in Domain Driven Design is to use Value Objects. A Value Object has no identifier attached to it; only the combination of the values of its properties gives it any identification. If you change any of the properties in any way, the modification must return an entirely new instance of the Value Object.
This kind of behavior means the Value Object is “immutable.” That is, the particular instance is not allowed to change, though you can get back a new instance with modified values. The code for an immutable object looks something like this:
<?php
class ImmutableFoo
{
protected $bar;
public function __construct($bar)
{
$this->bar = $bar;
}
public function getBar()
{
return $this->bar;
}
public function withBar($newBar)
{
$clone = clone $this;
$clone->bar = $newBar;
return $clone;
}
}
?>
(Note how $bar
is accessible only through a method, not as a public property.)
When you create an ImmutableFoo
instance, you cannot change the value of $bar
after instantiation. Instead, you can only get back a new instance with the new value of $bar
by calling withBar()
:
<?php
$foo = new ImmutableFoo('a');
$newFoo = $foo->withBar('b');
echo $foo->getBar(); // 'a'
echo $newFoo->getBar(); // 'b'
var_dump($foo === $newFoo); // (bool) false
?>
With this approach, you are guaranteed that one place in the code cannot change the $foo
object at a distance from any other place in the code. Anything that ever gets that instance of $foo
knows that its properties will always be the same no matter what.
The immutability approach can be powerful in Domain Driven Design and elsewhere. It works very easily in PHP with scalar values and nulls. That’s because PHP returns those by copy, not by reference.
However, enforcing immutability in PHP is difficult when the immutable object properties are non-scalar (i.e., when they are streams, objects, or arrays). With non-scalars, your object might seem immutable at first, but mutablity reveals itself later. These objects will be “quasi-“, not truly, immutable.
Streams as Immutable Properties
If a stream or similar resource has been opened in a writable (or appendable) mode, and is used as an immutable property, it should be obvious that object immutability is not preserved. For example:
<?php
file_put_contents('/tmp/bar.txt', 'baz');
$foo = new ImmutableFoo(fopen('/tmp/bar.txt', 'w+'));
$bar = $foo->getBar();
fpassthru($bar); // 'baz'
rewind($bar);
fwrite($bar, 'dib');
rewind($bar);
fpassthru($foo->getBar()); // 'dib'
?>
As you can see, the effective property value has changed, meaning immutability has been compromised.
One way around this might be to make sure that immutable objects themselves check that stream resources are always-and-only in read-only mode. However, even that is not a certain solution, because the resource pointer might be moved by reading operations in different parts of the application code. In turn, that means reading from the stream may yield different results at different times, making the value appear mutable.
As such, it appears that only “read-only” streams can be used as immutable properties, and then only if the immutable object restores the stream, its pointers, and all of its meta-data to their initial state every time the stream is accessed.
Objects as Immutable Properties
Because PHP returns objects as references, rather than as copies, using an object as a property value compromises the immutability of the parent object. For example:
$foo = new ImmutableFoo((object) ['baz' => 'dib']);
$bar = $foo->getBar();
echo $bar->baz; // 'dib'
$bar->baz = 'zim';
echo $foo->getBar()->baz; // 'zim'
As you can see, the value of $bar
has changed in the $foo
instance. Any other code using $foo
will see those changes as well. This means immutability has not been preserved.
One way around this is to make sure that all objects used as immutable properties are themselves immutable.
Another way around this is to make sure that getter methods clone any object properties they return. However, it will have to be a recursively deep clone, covering all of the cloned object’s properties (and all of their properties, etc.). That’s to make sure that all object properties down the line are also cloned; otherwise, immutability is again compromised at some point.
Arrays as Immutable Properties
Unlike objects, PHP returns arrays as copies by default. However, if an immutable object property is an array, mutable objects in that array compromise the parent object’s immutability. For example:
$foo = new ImmutableFoo([
0 => (object) ['baz' => 'dib'],
]);
$bar = $foo->getBar();
echo $bar[0]->baz;
$bar[0]->baz = 'zim';
echo $foo->getBar()[0]->baz; // 'zim'
Because the array holds an object, and because PHP returns objects by reference, the contents of the array have now changed. That means $foo
has effectively changed as well. Again, immutability has not been preserved.
Likewise, if the array holds a reference to a stream resource, we see the problems described about streams above.
The only way around this is for the immutable object to recursively scan through array properties to make sure that they contain only immutable values. This is probably not practical in most situations, which means that arrays are probably not suitable as immutable values.
Settable Undefined Public Properties
Finally, PHP allows you to set values on undefined properties, as if they were public. This means it is possible to add mutable properties to an immutable object:
$foo = new ImmutableFoo('bar');
// there is no $zim property, so PHP
// creates it as if it were public
$foo->zim = 'gir';
echo $foo->zim; // 'gir'
$foo->zim = 'irk';
echo $foo->zim; // 'irk'
Immutability of the object is once again compromised. The only way around this is to impelement __set()
to prevent setting of undefined properties.
Further, it might be wise to implement __unset()
to warn that properties cannot be unset.
Conclusion
If you want to build a truly immutable object in PHP, it appears the best approach is the following:
- Default to using only scalars and nulls as properties.
- Avoid streams as properties; if a property must be a stream, make sure that it is read-only, and that its state is restored each time it is used.
- Avoid objects as properties; if a property must be an object, make sure that object is itself immutable.
- Especially avoid arrays as properties; use only with extreme caution and care.
- Implement
__set()
to disallow setting of undefined properties.
- Possibly implement
__unset()
to warn that the object is immutable.
Overall, it seems like immutability is easiest with only scalars and nulls. Anything else, and you have a lot more opportunity for error.
Remember, though, there’s nothing wrong with fully- or partially-mutable objects, as long as they are advertised as such. What you want to avoid are quasi-immutable objects: ones that advertise, but do not deliver, true immutability.
(For some further reading, check out At What Point Do Immutable Classes Become A Burden?)
Update 1:
Update 2:
UPDATE 3: Beware Leaking Mutability.
Read the Reddit discussion about this post here.