Value Objects
Value objects help encapsulate the business logic and uniqueness of your domain.
These are objects that (unlike entities) have no identity and are always immutable - meaning their state cannot change after they have been constructed. They are used to describe the characteristics of an entity, and to define data types specific to the domain that are not represented by primitives.
This package provides no specific tooling for value objects, because they should be written according to the logic of your domain. However, this chapter provides some examples and guidance on best practices.
Examples
TIP
PHP 8 readonly public
properties are great for the value object use-case. They ensure immutability, while removing the need to write lots of boilerplate "getter" methods.
Scalars
One use of a value object is to wrap a scalar value, to enforce business logic. For example, an EmailAddress
value object could be used to ensure that a string is always a valid email address.
namespace App\Modules\EventManagement\Domain\ValueObjects;
use CloudCreativity\Modules\Toolkit\Contracts;
final readonly class EmailAddress
{
public function __construct(public string $value)
{
Contracts::assert(
filter_var($this->value, FILTER_VALIDATE_EMAIL) !== false,
'Invalid email address.',
);
}
}
Multiple Values
It is also possible for value objects to represent the combination of multiple values.
In the example from the previous chapter, the Attendee
aggregate root has a Customer
value object. If the business logic is that a customer is defined by the combination of their first name, last name and email address, then the Customer
value object would be defined as follows:
namespace App\Modules\EventManagement\Domain\ValueObjects;
final readonly class Customer
{
public function __construct(
public string $firstName,
public string $lastName,
public EmailAddress $this->value,
) {
}
}
WARNING
It can be tempting to define value objects in a generic namespace or package, that can be required by multiple different bounded contexts. For example, defining a customer value object and sharing it across multiple bounded contexts.
This should typically be avoided. That is because, in the example above, the customer value object defines the nature of a customer specifically in the event management bounded context. Customer values could be different in other bounded contexts, for example in an order delivery context, the customer value object might need to also hold an address.
Enumerations
PHP 8 introduced enumerations, or enums for short. These are perfect for domain value objects, as even the PHP docs describe:
Enumerations, or "Enums" allow a developer to define a custom type that is limited to one of a discrete number of possible values. That can be especially helpful when defining a domain model, as it enables "making invalid states unrepresentable."
Make good use of these. For example, if we needed to define the attendance status of an attendee:
enum AttendanceStatus: string
{
case Unconfirmed = 'unconfirmed';
case Confirmed = 'confirmed';
case Cancelled = 'cancelled';
}
Iterables
If you need to represent a collection of values, you can use a value object to encapsulate the collection and enforce business logic. For example, a ListOfTickets
value object could be used to ensure that a collection only contains ticket entities, and that these are keyed as a zero-indexed list.
Having specific value object classes for these kinds of collections can be useful, as it allows you to encapsulate business logic specific to the collection.
For example:
namespace App\Modules\EventManagement\Domain;
use CloudCreativity\Modules\Contracts\Toolkit\Iterables\ListIterator;
use CloudCreativity\Modules\Toolkit\Iterables\IsList;
/**
* @implements ListIterator<Ticket>
*/
class ListOfTickets implements ListIterator
{
/** @use IsList<Ticket> */
use IsList;
public function __construct(Ticket ...$tickets)
{
$this->stack = $tickets;
}
public function allCancelled(): bool
{
foreach ($this->tickets as $ticket) {
if ($ticket->isNotCancelled()) {
return false;
}
}
return true;
}
}
TIP
See the Iterables Toolkit Chapter for an explanation of the tooling provided to help you write iterable value objects.
Invariants
Value objects should enforce invariants. This means that they should always be in a valid state, and should throw an exception if they are ever constructed with invalid data.
For example, our Customer
value object ensures that a customer always has a first name, last name and email address. If it is successfully constructed, then this should always be true.
However, there is a flaw in the example given. Both the first and last name are type-hinted as a PHP string
. This means that they can be constructed with an empty string, which is not valid. The constructor should therefore enforce this invariant:
use CloudCreativity\Modules\Toolkit\Contracts;
final readonly class Customer
{
public function __construct(
public string $firstName,
public string $lastName,
public EmailAddress $email,
) {
Contracts::assert(!empty($this->firstName), 'First name cannot be empty.');
Contracts::assert(!empty($this->lastName), 'Last name cannot be empty.');
}
}
TIP
See the Assertions Chapter for an explanation of the Contracts::assert()
helper.
Immutability
Value objects must always be immutable. This means that once they have been constructed, their state cannot change. They can then be freely copied and shared, without fear of the consistency or state of the domain being accidentally altered.
If there are scenarios where you want to provide a way to mutate the state of a value object via a method, you must always ensure the method returns a new instance - with the original instance unaltered.
For example, let's say our business logic allowed for a customer's email to be changed. This would be incorrect:
final readonly class Customer
{
public function __construct(
public string $firstName,
public string $lastName,
public EmailAddress $email,
) {
}
public function setEmail(EmailAddress $email): self
{
$this->email = $email;
return $this;
}
}
Here the setEmail
method changes the email value on the current instance of the Customer
value object, mutating the state and breaking immutability. Instead, the method should return a new instance:
final readonly class Customer
{
public function __construct(
public string $firstName,
public string $lastName,
public EmailAddress $email,
) {
}
public function withEmail(EmailAddress $email): self
{
return new self(
firstName: $this->firstName,
lastName: $this->lastName,
email: $email,
);
}
}
TIP
What you call your methods on your value objects is up to you. Here we've used withEmail()
intentionally as a convention to indicate that the method returns a new instance. As opposed to setEmail()
which could imply the email will be set (overwritten) on the current instance.
This convention isn't compulsory. However, ensuring you consistently name your methods across all value objects will make your codebase predictable and easier to understand.
DateTimes
PHP provides a DateTime
class, but it is not immutable. You should never use this, because it breaks the immutability principal of value objects.
Instead, use PHP's DateTimeImmutable
class, which does guarantee immutability.
If you are using a package that builds on these native classes, be careful about immutability. For example, the popular Carbon package provides both Carbon
and CarbonImmutable
classes. You should only ever use CarbonImmutable
.
WARNING
Also watch out for using DateTimeInterface
. If you type-hint a value as this, you cannot be sure that the value you are given is immutable. If you must use the interface, immediately cast the value using DateTimeImmutable::createFromInterface()
to ensure you definitely have an immutable date-time:
public function __construct(DateTimeInterface $createdAt)
{
$this->createdAt = DateTimeImmutable::createFromInterface($createdAt);
}
Generic Collections
Some PHP frameworks provide "generic" classes for handling arrays, such as Laravel's Collection.
These are typically mutable, and therefore should not be used to represent state in your domain. Even if they were immutable, they are not specific to your domain - and add uncertainty to your codebase by potentially holding anything.
Instead, you should use immutable value objects to represent collections that hold specific data - such as the ListOfTickets
example given earlier. These are advantageous, because you can use them to encapsulate business logic about that specific collection of data.
Equality
Value objects must always have object equality. This means that two value objects are considered equal if they have the same values, even if they are different instances.
In PHP, a good tip is to put the equality logic in an equals()
method. This gives you a fluent interface for checking whether two values are equally. Additionally, you can then use PHPUnit's assertObjectEquals()
assertion to ensure two value objects are equal in tests - which is a nice benefit.
For example, we can improve the example customer value object as follows:
final readonly class Customer
{
public function __construct(
public string $firstName,
public string $lastName,
public EmailAddress $email,
) {
}
public function equals(self $other): bool
{
return $this->firstName === $other->firstName &&
$this->lastName === $other->lastName &&
$this->email->equals($other->email);
}
}
Serialization
It can be tempting to implement PHP's JsonSerializable
or Stringable
interfaces on value objects, so that they can be easily serialized to JSON or strings. However, this is not recommended.
The reason is that when you implement either on a value object, you have no context to understand why the object is being serialized, and how it should be serialized.
This can be illustrated with our example Customer
value object. How should it be represented in JSON? Like this:
{
"first_name": "Frankie",
"last_name": "Manning",
"email": "frankie.manning@example.com"
}
Or like this?
{
"name": {
"first": "Frankie",
"last": "Manning"
},
"email": "frankie.manning@example.com"
}
Only the presentation layer knows if the JSON is being used as a data delivery mechanism. Or the infrastructure layer, if the JSON is being used as a storage or communication format. As we know, the domain layer is the inner-most layer and should have no knowledge of the other layers.
The same applies for serializing to strings - this is a concern of the presentation or infrastructure layers.
The only exception to this would be scalar value objects - for example our EmailAddress
value object. In this case, it is reasonable to implement JsonSerializable
as the only possible representation of the value in JSON is its underlying scalar value - in this case, a string. It would also be reasonable to implement Stringable
for the same reason.
INFO
The logic for this exception to the rule matches how PHP handles BackedEnum
s - which in JSON are serialized to their backing scalar value (either a string or integer).
So our EmailAddress
class could look like this:
namespace App\Modules\EventManagement\Domain\ValueObjects;
use CloudCreativity\Modules\Toolkit\Contracts;
final readonly class EmailAddress implements \Stringable, \JsonSerializable
{
public function __construct(public string $value)
{
Contracts::assert(
filter_var($this->value, FILTER_VALIDATE_EMAIL) !== false,
'Invalid email address.',
);
}
public function __toString(): string
{
return $this->value;
}
public function jsonSerialize(): string
{
return $this->value;
}
}
TIP
As always, consistency in your approach is key. If you decide to implement JsonSerializable
or Stringable
on your scalar value objects, ensure you do so consistently across your domain. Predictability for developers is worth its weight in gold!
Testing
Value objects should not be mocked in tests. This is because they enforce invariants, ensuring they can never be incorrectly constructed. Mocking them breaks this rule, as a mock could be configured with invalid values.
Luckily, if you write your value objects like the examples in this chapter, you'll find that you cannot mock them in unit tests. Declaring a class final
and/or readonly
both prevent a class from being mocked.