DEV Community

Steve Crow
Steve Crow

Posted on • Originally published at smcrow.net on

Immutable PHP Objects Using Builders

The other day I was discussing some of the problems that can occur when building data objects in PHP. My experience so far has led me to believe that data objects in PHP are a fairly new concept, because the PHP language:

  1. Isn’t Strongly typed – leading to a lot of array abuse.
  2. Allows for magic assignment – which means an object’s defined structure can change during the process of code execution.

Consider your basic POPO, or P lain O ld P hp O bject:

class Person {
    protected $firstName;
    protected $lastName;

    public function setFirstName(string $firstName): void
    {
        $this->firstName = $firstName;
    }

    public function setLastName(string $lastName): void
    {
        $this->lastName = $lastName;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }
}

This object represents a person, with all the basic identifiers. You can instantiate a person and set the first and last name. However, this POPO is completely mutable, you are allowed to call the setters and modify the information at any point in time. In fact, this object doesn’t have a constructor with arguments, so in order to instantiate it, you need to use the setters provided.

Introducing Immutability

During our discussion, there was a need for immutability on the object. We wanted to give the user a way to construct a Person object without being able to modify the object once it’s constructed. There are a few patterns that could be utilized here, but to keep things as simple as possible the class became this:

class Person {
    protected $firstName;
    protected $lastName;

    public function __construct(string $firstName, string $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }
}

The setters have been banished. The resulting object will allow you to set the properties on instantiation and they will not be modified without creating a new object. You knew this, though, this is object oriented programing with constructors 101. This is a perfectly acceptable pattern. A person is required to have a first and last name in our system, and it is strictly enforced with this practice.

Evolving the Person

What happens when the person starts to grow up? What happens when they begin to age? What about middle names, height, weight, gender, any other property that we might want to track on the person? What if only some of these fields are required? That leaves us with something like this:

class Person {
    protected $firstName;
    protected $lastName;
    protected $middleName;
    protected $age;
    protected $gender;
    protected $height;
    protected $weight;

    public function __construct(
        string $firstName,
        string $lastName,
        ?string $middleName = null,
        int $age = 0,
        ?string $gender = null,
        int $height = 0,
        int $weight = 0
    ) {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->middleName = $middleName;
        $this->age = $age;
        $this->gender = $gender;
        $this->height = $height;
        $this->weight = $weight;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getMiddleName(): ?string
    {
        return $this->middleName;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function getGender(): ?string
    {
        return $this->gender;
    }

    public function getHeight(): int
    {
        return $this->height;
    }

    public function getWeight(): int
    {
        return $this->weight;
    }
}

Can you recognize the inherent problem here? The number of constructor arguments has approached an unacceptable level. Not only that, but this is hard to use. If I wanted to represent somebody without a middle name, but with an age, I would have to do something like:

$person = new Person('John', 'Doe', null, 32);

But Steve, you’re saying, we must maintain immutability! Isn’t there a way to build such an object while still making its properties read-only? There is!

Builders

Builders can be used to, well, build classes. You can instruct them, commonly using fluent interfaces, on which properties of a class to set. Then, you call the build() method, and the newly constructed object is returned to you. Here’s an example of a PersonBuilder that doesn’t work:

class PersonBuilder {
    protected $person;

    public function __construct(string $firstName, string $lastName)
    {
        $person = new Person($firstName, $lastName);
    }

    public function middleName()
    {
        $this->person->middleName = $middleName;
        return $this;
    }

    // repeat for other properties

    public function build()
    {
        return $person;
    }
}

We start by constructing the Person with its required fields. You could circumvent this by removing the constructor from Person but I feel like this ensures that the required fields are still supplied.

We then have a host of methods that set the indicated property on the Person, and a build() method which returns the result. Notice the problem? That’s correct, PersonBuilder doesn’t have access to Person because it is immutable. We need a way to store the information on the builder because once the person has been constructed nothing is allowed to mutate it.

First Idea: Storing the properties in an array

Actually, I hadn’t thought of this idea until I started to write this post. I’m not completely sold on it, but I don’t think it’s a bad idea. We can use an array on the PersonBuilder to store the properties that we will eventually use to construct the Person.

class PersonBuilder {
    private $properties = [];

    public function __construct(string $firstName, string $lastName)
    {
        $this->properties['firstName'] = $firstName;
        $this->properties['lastName'] = $lastName;
    }

    public function middleName(string $middleName)
    {
        $this->properties['middleName'] = $middleName;
        return $this;
    }

    public function age(int $age)
    {
        $this->properties['age'] = $age;
        return $this;
    }

    // repeat for other properties

    public function build()
    {
        return new Person(
            $this->properties['firstName'],
            $this->properties['lastName'],
            $this->getPropertyOrDefault('middleName'),
            $this->getPropertyOrDefault('age', 0),
            $this->getPropertyOrDefault('gender'),
            $this->getPropertyOrDefault('height', 0),
            $this->getPropertyOrDefault('weight', 0)
        );
    }

    private function getPropertyOrDefault($key, $default = null)
    {
        return array_key_exists($key, $this->properties) ? $this->properties[$key] : $default;
    }

The usage here would now be: $person = (new PersonBuilder('John', 'Doe'))->age(25)->build();

Note: Instead of using an array you could magically set the properties on the builder since arrays in PHP are kind of like objects. I’m not a big fan of magic properties, though it does mitigate the need for the getPropertyOrDefault() method.

This isn’t too bad, but it does require that we make another class, and we have to store the properties.

Second Idea: Storing the properties as properties

Another possibility would be to store the properties on the builder as, well, properties. I touched on this in the first idea so I won’t go into too much detail, but I feel like this essentially creates a duplicate object. One way to get around this would be to use inheritance. Something along the lines of this:

class PersonBuilder extends Person {
    public function middleName(string $middleName)
    {
        $this->middleName = $middleName;
        return $this;
    }

    public function age(string $age)
    {
        $this->age = $age;
        return $this;
    }

    public function build()
    {
        // Required Fields
        $person = new Person($this->firstName, $this->lastName);

        // Optional Fields
        foreach (get_object_vars($this) as $key => $value) {
            $person->{$key} = $value;
        }

        return $person;
    }
}

This is an interesting approach. I’m not completely sold on it because, again, I think it abuses inheritance in a non-ideal way. It certainly gives us access to the Person variables directly, and even allows us to iterate over the properties of the Person class and assign them. We still have to create a separate object that is tied to the Person which is also a Person.

Third Idea: Inner Builder

In Java, you’re able to define inner classes. Inner classes are contained within a class and have access to all of the properties of the outer class. It’s not uncommon to use an inner Builder class to construct the outer class. PHP 7 introduced anonymous classes which I think we can utilize in a similar fashion. It isn’t perfect, and it has the same inheritance headache as the second idea, but I still thought it was pretty neat.

Here’s what I came up with:

class Person {
    protected $firstName;
    protected $middleName;
    protected $lastName;
    protected $age;
    protected $gender;
    protected $height;

    protected function __construct(string $firstName, string $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getMiddleName(): string
    {
        return $this->middleName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function getGender(): string
    {
        return $this->gender;
    }

    public function getHeight(): int
    {
        return $this->height;
    }

        public static function Builder(string $firstName, string $lastName)
    {
        return new class($firstName, $lastName) extends Person {

            public function __construct(string $firstName, string $lastName)
            {
                $this->firstName = $firstName;
                $this->lastName = $lastName;
            }

            public function build()
            {
                $person = new parent($this->firstName, $this->lastName);
                foreach (get_object_vars($this) as $key => $value) {
                    $person->{$key} = $this->{$key};
                }
                return $person;
            }

            public function lastName(string $lastName)
            {
                $this->lastName = $lastName;
                return $this;
            }

            public function age(int $age)
            {
                $this->age = $age;
                return $this;
            }

            public function gender(string $gender)
            {
                $this->gender = $gender;
                return $this;
            }

            public function height(int $height)
            {
                $this->height = $height;
                return $this;
            }
        };
    }
}

By utilizing a static method that returns an anonymous child of the Person class we can internalize our builder. While this still abuses instantiation and technically it does return something that isa Person I feel that this internalization is still a neat concept. Our person can now be constructed fluently using:

$person = Person::Builder('John', 'Doe')->age(18)->height(60)->build();

We didn’t have to go through a gross constructor with lots of optional arguments, and the resulting Person is immutable.

Conclusion

This is just a fun idea I had, I’m sure that there’s a bigger discussion to be had about POPOs being immutable, and whether or not this over complicates things (spoilers: it does). Either way, it was a fun experiment with an interesting, albeit weird, application for anonymous classes in PHP.

Top comments (1)

Collapse
 
byhbt profile image
Thanh Huynh (Thành)

Hi Steve!

I would like to share another discussion about this topic here, hope it will give read more example on how to reduce the complexities of the constructor.

stackoverflow.com/questions/740527...

Thank you for sharing!