Photo by AltumCode on Unsplash

Value Objects

Lukasz

--

This article discusses the use of Value Objects in the context of PHP applications. The article presents step-by-step implementations of an example Value Object (class Person), as well as the use and discussion of the lbacik\value-object library. Presented examples utilise the phpspec framework as the test platform (one does not live by PHPUnit alone, after all!), and the entire code is available on GitHub. Enjoy!

The definition of Value Object can be found in many publications, including, of course, Wikipedia. I would like to begin with a quote from the book Domain-Driven Design in PHP (it’s not a definition, but rather a pointer towards some — interesting to me — qualities).

One of the main goals of Value Objects is also the holy grail of Object-Oriented design: encapsulation. By following this pattern, you’ll end up with a dedicated location to put all the validation, comparison logic, and behavior for a given concept.

Other publications you can refer to when looking for information on Value Objects:

  • Domain-Driven Design, Eric Evans
  • Implementing Domain-Driven Design, Vaughn Vernon
  • Patterns of Enterprise Application Architecture, Martin Fowler

In the context of this article, out of the features of Value Objects mentioned in these publications, I will focus on the following:

  • Immutability
  • Replaceability
  • Value Equality
  • Side-Effect-Free Behavior

In addition, a key (in my opinion) feature of Value Objects is the entry data validation mentioned in the opening quote. I mean here particularly the rule that a Value Object cannot be created for erroneous (incorrect) data, i.e. if an object exists, data is correct!

Execution

The form I decided to adopt is based on testing all the discussed functions in parallel to their presentation. For each function I will first write an appropriate test and then present a code which passes the test (the red/green/refactor mantra usually associated with the TDD approach). phpspec itself is referred to as BDD (Behaviour-Driven Development) framework — though BDD originated from TDD and all rules associated with TDD are fully respected.

An example Value Object is a Person class representing basic data describing some person:

  • name
  • age

As a reminder — since we are discussing Value Objects, we have no identity here (the Person object is not an entity, a record in a database, hence – among other things – it has no ID).

Setup

Let’s create a new (empty) directory for our tests and then start configuring the project.

phpspec installation:

$ composer req --dev phpspec/phpspec

Let’s define our test application’s namespace in composer.json (here: App):

{
"require-dev": {
"phpspec/phpspec": "^7.2"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
}

We will also configure phpspec — it is enough to add a phpspec.yml file with the following contents to the main directory:

suites:
acme_suite:
namespace: App
psr4_prefix: App

The last thing to do is to update the autoloader:

$ composer dump-autoload

Test no. 1

With phpspec, using the red/green/refactor rule becomes practically second nature. Starting off with the test — first, I instruct the framework that an App\Person class should exist in the app (our test Value Object):

$ vendor/bin/phpspec describe 'App\Person'
Specification for App\Person created in /.../spec/PersonSpec.php.

phpspec has created a test (a specification, available in the spec directory – the tests directory is not being used here). The created test (specification) verifies the existence of the indicated class – let’s run it:

$ vendor/bin/phpspec run

The call will fail — the required class (Person) does not exist yet (at this stage there is even no src directory). We’ll get a notification that the test failed but also a question if a class which was missing should be created. If we answer Yes, both a src directory and file with a Person class will be created.

Currently, the Person class will look like this:

namespace App;

class Person
{
}

Basic checks

We have the most important components — specification, class file, and we know how to run tests (the phpspec run command). In addition, every current test (for now only the verification if a Person class exists) runs correctly!

Let’s disturb this perfect balance. Let’s add the following code to the specification (spec/PersonSpec.php):

private const NAME = 'foo';
private const AGE = 30;

public function let(): void
{
$this->beConstructedWith(self::NAME, self::AGE);
}

The let method is an equivalent of PhpUnit’s setUp method (plus/minus) – i.e. indication of an action performed before each test. We want our Value Object to represent a person and have two parameters – name and age. We want these parameters to be passed when creating instances of our Person object – hence the beConstructedWith call in our let method. Those familiar with PhpUnit may immediately see a difference in approach towards the test – here, the specification is also an instance of the tested class!

Let’s verify our specification:

$ vendor/bin/phpspec run

The test will fail — there is no constructor in our class — we need to add one:

class Person
{
public function __construct(
public string $name,
public int $age,
) {
}
}

Now the test will change to green.

We add a test to check if the values passed in the constructors have actually been assigned to the correct properties (the PersonSpec.php file):

public function it_contains_data(): void
{
$this->name->shouldEqual(self::NAME);
$this->age->shouldEqual(self::AGE);
}

phpspec gives us some intuitive possibilities, doesn’t it? By the way — snake_case in the method’s name is a phpspec convention. If we check the compliance of our code with PSR-12 using phpcs we need to add an exception from the standard (in the configuration file ) for classes in the spec directory. I put an example of phpcs configuration in the repository https://github.com/lbacik/value-object-spec.

Let’s run a verification. There shouldn’t be any errors. The Person class takes on the values for the $name and $age parameters in the constructor and saves them in the corresponding fields. “So far, so good”.

But! A Value Object should be an immutable object (immutability is the first feature mentioned in the introduction). The user must not change the values in an already created object. Let’s illustrate it with the following check (in the PersonSpec class):

public function its_props_are_ro(): void
{
try {
$this->name = 'bar';
} catch (\Throwable $e) {
}
$this->name->shouldEqual(self::NAME);
}

An attempt to assign a new value to the $name field (generally every field, but this test only concerns the $name field) should generate an exception (ignored in this particular test – it could probably be written… more neatly but I haven’t found a way). In the final line of the test I check if the $name field value has actually remained unchanged (that is – in this case – if the value remains equal to the value passed to the Person class constructor in the specification’s let method).

Let’s run a verification (phpspec run).

We get an error message — our fields are editable — and the value in the $name field can be changed without a problem. How do we correct this? Since PHP 8.1 we have been able to set the parameters of the Person class constructor as readonly:

class Person
{
public function __construct(
public readonly string $name,
public readonly int $age,
) {
}
}

And the test is passed :)

In the PHP versions before 8.1 the only thing we could do was to set the visibility of our fields to private or protected and create the appropriate getters.

Set

Value Objects are immutable. This does not mean, however, that we can’t perform assignment operations — simply put, an attempt to assign a new value to a pre-existing object should end in a new object being created! This feature (called Replaceability) was described in Domain-Driven Design in PHP as follows:

This kind of behavior is similar to how basic types such as strings work in PHP. Consider the function strtolower. It returns a new string rather than modifying the original one. No reference is used; instead, a new value is returned.

Let’s assume that assignment is realised through the set method – let’s write a corresponding test

public function it_has_set_method(): void
{
$newPerson = $this->set(age: 40);
$newPerson->age->shouldEqual(40);
$this->age->shouldEqual(self::AGE);
}

Here I use named arguments, so PHP 8 is required. In the set method we can specify any number of arguments; in the example, we only specify the value for the $age field – in this case, in the newly created $newPerson object (the set method returns a new Person object), the value in the $name field remains unchanged from the $this object (the object with the $PersonSpec specification).

Verification (phpspec run) will return an error – a set method doesn’t exist in our Person class. Do not create it automatically! We’ll use its implementation defined in the lbacik/value-object library. Let’s install the library:

$ composer req lbacik/value-object

Now we can update the Person class to make it inherit from the ValueObject class defined in the installed library as \Sushi\ValueObject:

use Sushi\ValueObject;

class Person extends ValueObject
{
public function __construct(
public readonly string $name,
public readonly int $age,
) {
}
}

After rerunning the tests it turns out we have returned to the green state — now all tests should be passed!

In the lbacik/value-object it is assumed that we cannot create new fields with the set method (in new instances of a given class created with the method) – such attempts will generate a ValueObjectException. Let’s check it! Here’s a test:

public function it_has_the_same_keys(): void
{
$this
->shouldThrow(ValueObjectException::class)
->during('set', ['otherName' =>'foo']);
}

This notation verifies (unfortunately, in this particular case, the phpspec notation isn’t too… clear) if the execution:

$this->set(otherName: 'foo');

will generate a ValueObjectException, which should of course happen (running tests should confirm that).

isEqual

Another feature listed in the introduction is Value Equality — two objects (of the Value Object type) are assumed to be equal if they store the same values, not when they are identical. In other words, we are interested in comparing the values of all the properties of these objects. We don’t compare keys (which makes sense in the case of entities) or the addresses of these objects in the memory — we compare the value of each of their properties!

A test will show it better:

public function it_can_be_compared(): void
{
$person1 = new Person('Thor', 25);
$person2 = new Person(self::NAME, self::AGE);

$this->isEqual($person1)->shouldEqual(false);
$this->isEqual($person2)->shouldEqual(true);
}

The test should be passed immediately — the isEqual method was also defined in the \Sushi\ValueObject class.

A note: comparing within the isEqual method is realised by comparing the values of the same properties of two objects. Generating hash values and comparing only hashes is not utilised. Optimisation by generating and comparing hashes is a topic for other library versions.

Side-Effect-Free Behavior

In a nutshell — after creating a Value Object, the values of its arguments cannot change. This assumption eliminates any worries about side effects (the properties of our Value Object should not be in danger of being changed). Of course, we can point towards problematic situations — let’s consider, for example, a situation where the value of one of the fields of a Value Object in an object not fulfilling the same criteria, i.e. not inheriting (in the context of this article) from the ValueObject class. In this case, the values of the object’s fields will not have the same restrictions as the values of the fields of a Value Object. For example, in the case of the definition below:

class Gender
{
public const Female = 'female';
public const Male = 'male';

public function __construct(
public string $value,
) {
}
}

class Person extends ValueObject
{
public function __construct(
public readonly string $givenName,
public readonly string $familyName,
public readonly Gender $gender,
) {
}
}

The Gender class does not inherit from the ValueObject class – when comparing the Person objects, the reference to the Gender class will be included, not its value!

Conclusion: ideally, the only objects saved in the Value Object fields are other Value Objects!

If the Gender class in the above example inherited from the ValueObject class, the value parameter would be included when comparing Person-type objects!

I added tests illustrating these differences to the GitHub repository I mentioned in the introduction (it also contains the full code discussed in this article). The PersonWithGenderSpec (the Gender class, as above, does not inherit from ValueObject) and PersonWithCitySpec (where the City class inherits from the ValueObject class) tests.

Invariants

Now we have validation left. Type alone is absolutely not enough! Sometimes (often!), it will be necessary to use more advanced methods of validating data transferred when creating an object (remember the rule that an incorrect object should not be created in the first place).

In the case of the discussed lbacik/value-object library, the term invariant has been adopted to denote the rule that should be satisfied by the input data for an object to be created.

Every ValueObject-type class can contain any number of invariants.

Let’s analyse the test below:

public function it_is_an_adult_person(): void
{
$this
->shouldThrow(\Throwable::class)
->during('set', ['age' => 12]);
}

We are testing the following call:

$person->set(age: 12);

And expect the call to generate an exception, because we want to add an invariant to our Person Value Object, which ensures that all created objects represent people over 18 years of age (people exactly 18 years old will also fulfil the criterion).

Let’s look at the implementation:

use Sushi\ValueObject;
use Sushi\ValueObject\Exceptions\InvariantException;
use Sushi\ValueObject\Invariant;

class Person extends ValueObject
{
private const MIN_AGE_IN_YEARS = 18;

public function __construct(
public readonly string $name,
public readonly int $age,
) {
parent::__construct();
}

#[Invariant]
protected function onlyAdults(): void
{
if ($this->age < self::MIN_AGE_IN_YEARS) {
throw InvariantException::violation(
'The age is below ' . self::MIN_AGE_IN_YEARS
);
}
}
}

Observe the constructor — it has to contain the base class constructor call — this ensures checking the invariants!

Invariants themselves are methods with an assigned #[Invariant] tag (attribute). If such a method throws (any) exception, it means that the tested criterion was not fulfilled when creating an object.

Python’s simple-value-object library was the inspiration here. Of course, attributes (used here) work differently from Python’s decorators (used in sample-value-object), but the effect is (practically) identical – we have a tag (#[Invariant]), which we can used to mark all methods which should be called when creating an object. We can create any number of invariants, freely defining their rules.

Let’s take a look at another example:

public function its_name_is_not_too_short(): void
{
$this
->shouldThrow(\Throwable::class)
->during('set', ['name' =>'a']);
}

Let’s assume we don’t want the name attribute to be shorter than 3 characters. Of course, we can define the invariant similarly to the previous one, but let us consider another possibility.

Let’s add phpunit to our application:

$ composer req phpunit/phpunit

Important! We are adding the phpunit library without the --dev parameter of the composer command – so, we are adding it as a part of the application rather than an element of the development environment. This may appear odd though probably not to everybody – using assertion in an application code (not just in tests) appears to be quite popular (although I haven’t done any research on the topic). However, it is (probably) not popular enough for assertions to be delegated to some smaller library (to avoid adding the entire PHPUnit when we want to use the application for that one element).

Let’s see how we can use assertions in the Person class:

use function PHPUnit\Framework\assertGreaterThanOrEqual

...

private const NAME_MIN_LENGTH = 3;

...

#[Invariant]
public function checkName(): void
{
assertGreaterThanOrEqual(
self::NAME_MIN_LENGTH,
mb_strlen($this->name)
);
}

Let’s run tests — everything should be in perfect order!

Does using assertions give us any concrete advantage? I’ll leave it to the reader’s judgement.

Summary

Using Value Objects can significantly increase the readability of our code. Of course, this solution will not work every time, but in most cases it is worth considering.

The lbacik/value-object underwent a considerable metamorphosis in version 1.xa large amount of code from the previous versions was simply deleted. Not all problems have been (so far) solved in an… ideal way. The current version, though, is fully functional, and as they say: done is better than perfect – test away!

--

--