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.

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.

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.