Introducing the ConvenientImmutability package

Posted on by Matthias Noback

As was the case with the SymfonyConsoleForm library, I realized I had not taken the time to properly introduce another library I created some time ago: ConvenientImmutability. This one is a lot simpler, but from a technical standpoint I think it might be interesting to at least talk a bit about the considerations that went into it.

For starters, it's important to keep in mind that I don't think you'll actually need to use this library. Of course, I'll explain why. But first: what is ConvenientImmutability about?

In early 2016 I took a Java programming course. At some point they explained the concept of a final property. As Wikipedia puts it:

A final variable can only be initialized once [...]. It does not need to be initialized at the point of declaration: this is called a "blank final" variable.

Marking a property as final is really useful. It shows that, after some initial value has been assigned to it, it is not going to change again. In smarter-sounding words: such a variable becomes immutable. As you may know, using immutable values (maybe we shouldn't call them variables anymore) leads to code that's easier to understand and has less state-related bugs. It also brings us closer to writing pure functions that never modify existing values, only return new ones. Such functions, too, are easier to work with than their imperative counterparts, which change the value of variables all over the place (possibly even global ones) whenever they feel like it.

Defining final properties in PHP

Anyway, this is not a post about the merits of a functional programming style, this is about achieving "final properties" in PHP.

Of course, in PHP we can change the value of any property:

class SomeClass
{
    public $property;
}

$object = new SomeClass();

$object->property = 'some value';

// Re-assignment is not a problem
$object->property = 'some other value';

In fact, the property doesn't even need to be defined as a class attribute, you can just set it if you like:

// This will create a property on-the-fly:
$object->undefinedProperty = '...';

The magic __set() method

We'd like to be able to inject some code, right before a re-assignment, that warns the user: "you can't reassign this property". If we don't want to rely on code generation and auto-loader hijacking, there's nothing we can do, except … implement a "magic" __set() method:

class SomeClass
{
    /**
     * @param string $name The name of the property to set
     * @param mixed $value The value to assign to that property
     */
    public function __set($name, $value) {
        // if property "$name" has been assigned already:
        //     throw an exception

        // else:
        //     store the value in some internal map
    }
}
$object->property = 'some value';

// Re-assignment throws an exception:
$object->property = 'some other value';

The main problem is: __set() will only be called when the user tries to set a property that hasn't been defined as a class attribute. But we really need to make this work for properties that have been defined as class attributes.

Unsetting class attributes

Searching for a solution, I remembered that it was indeed possible to make PHP ignore defined attributes by unset-ting object properties (don't know where I learned this by the way):

class SomeClass
{
    // We can first define the attribute:
    public $property;

    public function __construct() {
        // Then unset it:
        unset($this->property);
    }

    public function __set($name, $value) {
        // ...
    }
}

$object = new SomeClass();

// This will trigger __set()
$object->property = 'some value';

We have the key to the solution, but we need several more ingredients:

  • The value passed to __set() should be kept safe inside a map (which can be an associative array).
  • It should be possible to retrieve the value from the pseudo-property. This can be achieved by implementing __set()'s counterpart: __get().
  • As a bonus we could also prevent the abuse of properties that are not defined, e.g. throw an exception when a user tries to assign a value to $object->undefinedProperty.

Completing the list of requirements

Of course, this should become a fully automated solution. We don't want to unset a list of hard-code properties.

Some other design goals I came up with:

  • The solution should use a trait instead of an abstract base class, to prevent the solution from taking over the inheritance tree the user has imagined for an object.
  • The solution should be designed to work well with DTOs. For regular objects, encapsulation of state is already a concern of many developers. For DTOs (i.e. objects with no behavior, only data) not so much.
  • It would be particularly nice if the solution works well with the Symfony Form component as it would allow us to have — for example — immutable command objects that can still be populated by the mutable-by-default property-mapping mechanism of the Form component.

The final solution (no pun intended)

It took some lines of code and some testing to make the solution generic, but take a look at the Immutable trait and you'll find all the ingredients mentioned above in that code.

Closing remarks

As I mention in the project's README, using this trait might help you overcome your fear of using public properties for DTOs. It would help you protect objects from being mutable (at least at the first line of defense, you could always store mutable objects in properties of a — thereby — semi-immutable object.

Since this may be more of a psychological thing, related to your mind's attachment to a certain state of the world, you may not be helped much by using this library. Still, it might be nice to know how it works.

PHP functional programming immutability
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).