In the last article we learnt how to create an immutable data structure in PHP. There were a few issues to work through, but we got there in the end. Now onto making the immutable class more useful and easier to create modified copies. Note that these are copies and not modifications, in-place, to the original objects.
This article is part of a series I have written on the topic of immutability in PHP code:
- Part one - a discussion of caveats and a simple scalar handling immutable
- Part two - improve the process of creating modified copies of the immutable
- Part three - objects in immutable data structures and a generalised immutable implementation
Also available in Русский (Russian):
Simple parameter mutations
When you want to modify a property in an immutable object you must, by definition, create a new object and insert the modified value into that new location. You could simply get the values from the current instance and pass them into a new instance as you create it.
$a = new Immutable('Test');
echo $a->getX(); // Test
$b = new Immutable($a->getX() . ' again');
echo $b->getX(); // Test again
So simple. Too simple!
This technique works reasonably well for this small dataset, but what if we had five or ten parameters that would have to be replayed every time? An exaggerated example to illustrate my point follows.
$a = new Immutable('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K');
echo $a->getK(); // K
$b = new Immutable(
$a->getA(), $a->getB(), $a->getC(), $a->getD(), $a->getE(), $a->getF(),
$a->getG(), $a->getH(), $a->getI(), $a->getJ(), $a->getK() . ' some change'
);
echo $b->getK(); // K some change
It is certainly doable, but I, for one, am not going to be executing that every time I need to work with an immutable instance. Certainly, not if I can avoid it.
Mutation at clone time
There is a very handy quirk in PHP that we can exploit. It will allow us to create new modified copies of the object in question.
Instead of mutating the object in place like you would in traditional OOP, we’re going to make a clone of the object and changes it’s private properties. Yes, you read that correctly, you can change the private properties of a class instance!
So you’ve probably learnt that private means that a class property cannot be changed from outside or by other classes overriding it. Whilst this is generally true; when we clone an object we get a fleeting opportunity to change it’s private properties.
$a = new Immutable('A', 'B');
echo $a->getB(); // B
$b = clone $a;
$b->B = '22';
// Fatal error: Cannot access private property Immutable::$B
Well that didn’t work! I should’ve said that you can only perform the clone from within a method of the same class to be able to modify it like this.
declare(strict_types=1);
final class Immutable {
private $x;
private $mutable = true;
public function __construct(string $input) {
if (false === $this->mutable) {
throw new \BadMethodCallException('Constructor called twice.');
}
$this->x = $input;
$this->mutable = false;
}
public function getX(): string {
return $this->x;
}
public function withX(string $input): Immutable {
$clonedClass = clone $this;
$clonedClass->x = $input;
return $clonedClass;
}
}
$a = new Immutable('TEST');
echo $a->getX(); // TEST
$b = $a->withX('noop');
echo $b->getX(); // noop
In this way it becomes easier to modify a value inside an immutable - we can wrap up the clone
and set
the right values for them. Having a shortened syntax like this really serves to help implementers work
with immutable objects.
Preventing the setting of unexpected properties
There are other ways that a seemingly immutable class can be messed with too. Fortunately, these can be stopped with a couple of PHP magic methods.
In PHP it is possible to add properties to a class at run time - even a final
class. We don’t want this
as it would change the shape of our class and therefore mean that it was mutable. The simple way to prevent this is to add an empty __set()
magic method implementation to your class.
public function __set(string $id, $val): void {
return;
}
It is also possible to remove property values by using the unset()
construct. We can also prevent this
using another empty magic method:
public function __unset(string $id): void {
return;
}
It is important to ban these. Whilst they do not allow modification of our private properties - they do allow outside agents to change our immutable class by adding and remove their own public properties. The class would no longer be immutable were this allowed to happen.
Merged clone time mutation
So far we’ve seen the ability to change one property using a withX
style method, but what if we want
to change more? Well, you could just chain the changes up with something like this.
$a = new MyFantasyImmutable('TEST', 'foo');
echo $a->getX(); // TEST
echo $a->getY(); // foo
$b = $a->withX('noop')->withY('bar');
echo $b->getX(); // noop
echo $b->getY(); // bar
Whilst it works, there are a few things that I dislike about this approach. A throwaway instance is
created between the calls to withX()
and withY()
, you have to create a with*()
function for every
property and the method chaining quickly gets irritating.
There is, of course, another way.
First, let’s define a new immutable class with a few properties.
declare(strict_types=1);
final class Bike {
private $engineCc, $brakes, $tractionControl;
private $mutable = true;
public function __construct(int $engineCc, string $brakes, bool $tractionControl) {
if (false === $this->mutable) {
throw new \BadMethodCallException('Constructor called twice.');
}
$this->engineCc = $engineCc;
$this->brakes = $brakes;
$this->tractionControl = $tractionControl;
$this->mutable = false;
}
public function __get($property) {
if (property_exists($this, $property)) {
return $this->$property;
}
}
public function __set(string $id, $val): void {
return;
}
public function __unset(string $id): void {
return;
}
}
To keep the example shorter, I’ve employed a small __get()
magic method instead of writing a get
method of each property the class. You would have to write one for each ending up with functions like
getEngineCc()
, getBrakes()
and getTractionControl()
. Instead you access them directly as
properties instead.
$zx9r = new Bike(900, '2 piston floating discs', false);
echo $zx9r->engineCc; // 900
echo $zx9r->brakes; // 2 piston floating discs
$cagivaRaptor = new Bike(1000, '2 piston floating discs', false);
var_dump($cagivaRaptor->tractionControl); // bool(false)
Anyway back to the mutations! To allow for the easy manipulation of the classes properties when cloning we can add a simple method to the class.
public function with(array $args): Bike {
$clonedClass = clone $this;
foreach($args as $property => $value) {
if (property_exists($clonedClass, $property)) {
$clonedClass->$property = $value;
}
}
return $clonedClass;
}
Now when you want a new class with modifications - perhaps when you’re releasing a new motorbike model -
you can just call with()
and include an associative array.
$zx9r = new Bike(900, 'Floating 2 piston', false);
$zx10r = $zx9r->with(['engineCc' => 1000, 'tractionControl' => true]);
echo $zx10r->engineCc; // 1000
echo $zx10r->brakes; // Floating 2 piston
var_dump($zx10r->tractionControl); // bool(true)
While it works OK, you may have noticed that we’ve now effectively eliminated the ability for PHP to type check the input. We’re no longer populating the class via the constructor.
This is bad because a non-scalar value could be passed in (more on why this sucks in a future article).
One way we could solve this is to replace with()
with a function that uses reflection to workout the
constructors parameter order and merge newly supplied values in.
public function with(array $args): Bike {
$reflection = new ReflectionMethod($this, '__construct');
$new_parameters = array_map(function($param) use ($args) {
$x = $param->name;
return (array_key_exists($x, $args))
? $args[$x] // use newly supplied value
: $this->$x; // fallback to the current value
}, $reflection->getParameters());
return new self(...$new_parameters);
}
When the new class instance is created the newly supplied values are passed to the constructor, which ensures that they’re correctly type checked.
You would call this method in the same way as the last with()
implementation. It does make the assumption
that the class properties will have the same name
as the constructor parameter name ($this->engineCc
is the same as constructor parameter $engineCc
for example).
This would leave you a final Bike
class of:
declare(strict_types=1);
final class Bike {
private $engineCc, $brakes, $tractionControl;
private $mutable = true;
public function __construct(int $engineCc, string $brakes, bool $tractionControl) {
if (false === $this->mutable) {
throw new \BadMethodCallException('Constructor called twice.');
}
$this->engineCc = $engineCc;
$this->brakes = $brakes;
$this->tractionControl = $tractionControl;
$this->mutable = false;
}
public function __get($property) {
if (property_exists($this, $property)) {
return $this->$property;
}
}
public function with(array $args): Bike {
$reflection = new ReflectionMethod($this, '__construct');
$new_parameters = array_map(function($param) use ($args) {
$x = $param->name;
return (array_key_exists($x, $args))
? $args[$x] // use newly supplied value
: $this->$x; // fallback to the current value
}, $reflection->getParameters());
return new self(...$new_parameters);
}
public function __set(string $id, $val): void {
return;
}
public function __unset(string $id): void {
return;
}
}
Also bear in mind that the reflection API provided by PHP is not crazily quick so if micro-optimisations are your thing then you’d probably want to avoid this. If you can take the hit then the extra security you get from the type checking is worth it.
Using a builder to generate immutable objects
Another way around this particular issue with immutable objects can be to use a second class to generate the immutable objects. This will allow you to avoid the use of the Reflection API and still give you the advantage of type checking. A little touch of irony here as we’ll use a mutable builder class to produce an immutable object, but bear with me.
Firstly, we need to define the immutable object our builder will produce. I am removing the __get()
magic here too
as our aim is to make it easier for our code to be analysed statically. This will help IDEs to type hint, code quality
tools to read our code and ostensibly make the code easier to follow cognitively.
declare(strict_types=1);
final class Bike {
private $engineCc, $brakes, $tractionControl;
private $mutable = true;
public function __construct(int $engineCc, string $brakes, bool $tractionControl) {
if (false === $this->mutable) {
throw new \BadMethodCallException('Constructor called twice.');
}
$this->engineCc = $engineCc;
$this->brakes = $brakes;
$this->tractionControl = $tractionControl;
$this->mutable = false;
}
public function getEngineCc(): int {
return $this->engineCc;
}
public function getBrakes(): string {
return $this->brakes;
}
public function getTractionControl(): bool{
return $this->tractionControl;
}
public function __set(string $id, $val): void {
return;
}
public function __unset(string $id): void {
return;
}
}
Now we have a simple little immutable class we can get on with the business of creating a generating class.
This new class will accept all the values we wish to store in the immutable class and return an instance of Bike
.
class BikeGenerator {
private $engineCc, $brakes, $tractionControl;
public static function create(): self {
return new self;
}
public static function with(Bike $oldBike): self {
$generator = new self;
$generator->setEngineCc($oldBike->getEngineCc());
$generator->setBrakes($oldBike->getBrakes());
$generator->setTractionControl($oldBike->getTractionControl());
return $generator;
}
public function setEngineCc(int $cc): self {
$this->engineCc = $cc;
return $this;
}
public function setBrakes(string $brakes): self {
$this->brakes = $brakes;
return $this;
}
public function setTractionControl(bool $tractionControl): self {
$this->tractionControl = $tractionControl;
return $this;
}
public function build(): Bike {
return new Bike($this->engineCc, $this->brakes, $this->tractionControl);
}
}
The BikeGenerator
duplicates some code of the original class and really serves as a glorified queue. We add
to the queue until we’re happy and execute build()
to be given a freshly populated instance of Bike
.
$zx9r = BikeGenerator::create()
->setEngineCc(900)
->setBrakes('2 piston floating disc')
->setTractionControl(false)
->build();
echo $zx9r->getEngineCc(); // 900
echo $zx10r->getBrakes(); // 2 piston floating disc
var_dump($zx10r->getTractionControl()); // bool(false)
$zx10r = BikeGenerator::with($zx9r)
->setEngineCc(1000)
->setBrakes($zx9r->getBrakes() . ' ABS')
->setTractionControl(true)
->build();
echo $zx10r->getEngineCc(); // 1000
echo $zx10r->getBrakes(); // 2 piston floating disc ABS
var_dump($zx10r->getTractionControl()); // bool(true)
This shows a use of the builder pattern to generate a ready made immutable Bike
instance. You can then call ::with()
to easily create a new modified version of an existing object.
Setting larger amounts of properties
There is not all that much that you can do remove the tedium of dealing with many values in an immutable with PHP. One way to get past this is to pass in an array of values that are checked and stored in the immutable.
declare(strict_types=1);
final class Config {
private $properties = [
// property => data type
// assume no type = string
'name',
'version' => 'int',
'released' => 'bool',
'licence',
'private' => 'bool',
'url',
'repo',
'downloads' => 'int'
];
private $data = [];
private $mutable = true;
public function __construct(array $values) {
if (false === $this->mutable) {
throw new \Exception('Constructor called twice.');
}
$this->set($values);
$this->mutable = false;
}
public function __get($property) {
if (array_key_exists($property, $this->data)) {
return $this->data[$property];
}
throw new \Exception('The property ' . $property . ' does not exist');
}
private function set(array $values) {
foreach($this->properties as $prop => $type) {
if (!is_string($prop)) {
// coalesce to string for properties that don't have a type specified
$prop = $type;
$type = 'string';
}
if (array_key_exists($prop, $values)) {
$this->setValue($prop, $type, $values[$prop]);
}
}
}
private function setValue($prop, $type, $value) {
$check = 'is_' . $type; // eg. is_int()
if ($check($value)) {
$this->data[$prop] = $value;
} else {
throw new \InvalidArgumentException('Incorrect type passed for the "' . $prop . '" property - expected ' . $type . ' , but got ' . gettype($prop));
}
}
public function __set(string $id, $val): void {
return;
}
public function __unset(string $id): void {
return;
}
}
We’ve had to eschew the type system in favour a small custom type check defined in the $properties
class property
and evaluated in the setValue()
method.
$c = new Config([
'name' => 'foo',
'version' => '10'
]);
// Uncaught InvalidArgumentException: Incorrect type passed for the "version" property - expected int , but got string
The way that this works also makes it easy to handle optional arguments at instantiation - up until this point all arguments
have been mandatory. Here you can supply an array missing one or more properties and they simply won’t be
set in the $this->data
array.
$c = new Config([
'name' => 'foo',
'version' => 10
]);
echo $c->name; // foo
echo $c->version; // 10
echo $c->repo; // Uncaught Exception: The property repo does not exist
There are further improvements that could be made here to make the class better able to handle non-existent values, but they’ll have to be the subject of another article.
Also, note that the way the set()
method is designed means that it will silently ignore properties passed to it
that do not exist in the $properties
class property. You may wish to change this to
throw an exception or warning depending on your use case, of course.
Merging larger collections of properties
It is a very simple exercise to perform a merge now that we have an array for the value store and we’re
accepting an associative array for input. We can add the following method to the Config
class.
public function with(array $values): Config {
return new self(array_merge($this->data, $values));
}
This can then be exercised with:
$c = new Config([
'name' => 'foo',
'version' => 10,
'repo' => 'github.com',
]);
echo $c->name; // foo
echo $c->version; // 10
echo $c->repo; // github.com
$c2 = $c->with(['name' => 'bar', 'version' => 12]);
echo $c2->name; // bar
echo $c2->version; // 12
echo $c2->repo; // github.com
By writing the code this way, we’ve effectively written our own little implementation of named parameters too. As great as that may be we’re also losing some clarity and IDE type hinting with the inherent indirection.
Anyway you cut it, manipulating an immutable in PHP can get annoying pretty quickly. There appears to be no really simple way of avoiding typing more and/or affecting the type hinting abilities of IDEs. These are some of the techniques I’ve used before to workaround some of the frustration, but there is definitely room for improvement.
This article is part of a series I have written on the topic of immutability in PHP code:
- Part one - a discussion of caveats and a simple scalar handling immutable
- Part two - improve the process of creating modified copies of the immutable
- Part three - objects in immutable data structures and a generalised immutable implementation
Also available in Русский (Russian):
If you like this article then you might get a kick out of writing functional php code as taught in the Functional Programming in PHP book that I wrote.