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.x
– a 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!