Skip to content

Bring Value to your code

Through its iterations, PHP has become as appropriate as any other language to express Domain-Driven Design and implement other, more complex, concepts and patterns. One of these, one of the most important building blocks in Domain Driven Design (DDD), is the Value Object... But what is a Value Object? Is it only useful in DDD? How, where and why should one use them? Let's try to check it out...

Info

phparchitect-cover_2022-11.png   phparchitect-cover_2022-12.png This article was first published in two parts in PHP[Architect] in the editions or November & December 2022.

Once more, kudos to Larry GARFIELD (@crell), who was kind enough to go through this article and made sure the biggest non-sense was corrected.

Eric Evans, in Domain-Driven Design1 -- also called the Blue Book or the DDD Bible -- gives the following definition of a Value Object:

An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are.

And in his Implementing Domain-Driven Design2, aka the Red Book, Vernon Vaughn dedicates a whole chapter to Value Objects and lists its characteristics:

  • It measures, quantifies, or describes a thing in the domain;
  • It should be immutable;
  • It captures a whole value;
  • It is replaceable;
  • It prevents side-effects.

That's all very DDD... But, as we'll see below, Value Objects are very useful even if one doesn't work with the Domain Driven Design paradigm.

Identity... Not!

Evans highlights the key characteristic of a Value Object, that it is defined by its value and not by its identity.

If you plan to paint your house in, let's say, pink, you go out to the store and buy a load of buckets of pink paint. Back home, you just pick the first bucket and start painting. Although they are different buckets, what you really care about is the color it contains. The value of the bucket is defined, not by its identity, but by the color it contains and, therefore, all the buckets are equal.

In PHP, this translates into objects that have no id and one or more attributes that define its value.
Side note: we are focusing on the PHP object for the moment, not how the data is stored in a database. This distinction is very important. We will address persistence later on.

As PHP developers, we all know at least one famous Value Object: DateTimeImmutable:

<?php
$now = new \DateTimeImmutable();

The DateTimeImmutable object has values for year, month, day, hour, minute, seconds and timezone ... but not a single property that sets an identity.

With PHP 8.1, Enums were introduced and they work pretty well as Value Objects:

<?php
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

As one can see, no identity... Value only.

Side-note: Enums are a little peculiar in regards of the identity characteristic. They are in fact Singletons and will therefore always return the same instance. So they kind of are their own identity value. This does not disqualify them as Value Objects. As one can read below, Enums definitely match with all the Value Object's criteria.

Last but not least, we can also define our own custom Value Objects:

<?php
class EmailAddress
{
    public function __construct(
        public string $value
    ){
    }
}

Again... no identity and only a value.

Side-note: the absence of an identity is the main difference between a Value Object and an Entity whether it is a Domain Driven Design Entity or an ORM Entity.

Rule 0

A Value Object is a thing that defines itself by its value not by its identity.

Descriptive

Another important keyword in Evans' definition above is descriptive. And Vaughn writes a Value Object describes, measures or quantifies something.

To handle these characteristics, a Value Object has one or more attributes:

<?php
class Color
{
    public function __construct(
        public int $red,
        public int $green,
        public int $blue,
        public float $alpha,
    ) {
    }
}

In the above code listing, we have a class that is able to define a color with the RBGA model. When talking about the color of light, like on a screen, Red, Green, Blue and Alpha are the four channels that define a color.

Every attribute of the Color class indicates each channel's exact intensity that is required to obtain the exact color we want:

<?php
$black = new Color(0, 0, 0, 1);
$white = new Color(255, 255, 255, 1);
$redTransparent = new Color(255, 0, 0, .25);

Interestingly enough, these attributes, instead of scalars, could also be other Value Objects. For instance:

<?php
enum Sharpness {
    case VerySharp;
    case NotSoSharp;
    case NotSharpAtAll;
}

enum Hardness {
    case OhSoHard;
    case RegularHard;
    case Medium;
    case Soft;
}

class VirtualPencil
{
    public function __construct(
        public Color $color,
        public Sharpness $sharpness,
        public Hardness $hardness,
    ) {
    }
}

$virtualPencilToDrawAMustachOnMarcInMeta = new VitualPencil(
                                                new Color(0, 0, 0, 1),
                                                Sharpness::VerySharp,
                                                Hardness::OhSoHard,
                                            );

In the above example, the VirtualPencil is defined by a Color, a Sharpness and a Hardness attribute, and all of them are themselves Value Objects.

Rule 1

A Value Object has one or more attributes whose values define the Value Object. The attributes are either primitives or other Value Objects.

Immutable

In order to share the value object within an application and avoid any side effects, it is highly recommended for them to be immutable so that they can't change all of a sudden.

This will avoid annoying, sometimes embarrassing, and always hard to debug issues like this:

<?php
class VirtualPencilExtensionHandler
{
    public function __construct(
        public Color $color,
    ) {
    }

    public function handleExtension(TheExtensionInterface $extension): void
    {
        $extension->run($this->color);
    }
}

class NastyExtensionThatChangesColor implements TheExtensionInterface
{
    public function run(Color $color): void
    {
        $color->blue = 255;
        // ...
    }
}

As one sees, now Marc will end up with a blue mustache if we were to use the set color later on in a virtual pencil... And nobody would want Marc to have a blue mustache, or would we?

More seriously, this shows that sometimes an object, or any referenced variable, when passed on to another scope, may get changed without the original scope being aware of it. As we can see above, this may cause quite some trouble.

This can easily be prevented by making the Value Object's attributes immutable. Easy in PHP 8.1, with the latest readonly property:

<?php
class Color
{
    public function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue,
        public readonly float $alpha,
    ) {
    }
}

and even easier in PHP 8.2 with readonly classes:

<?php
readonly class Color
{
    public function __construct(
        public int $red,
        public int $green,
        public int $blue,
        public float $alpha,
    ) {
    }
}

If you're not lucky enough to work with PHP's latest versions, you still can use private properties and public getter methods for the Value Objects, but obviously no public setter methods.

<?php
class Color
{
    private int $red;
    private int $green;
    private int $blue;
    private float $alpha;

    public function __construct(
        int $red, int $green, int $blue, float $alpha,
    ) {
    }

    public function red(): int 
    {
        return $this->red;
    }
    // etc
}

Side note: there are cases where one explicitly wants/needs mutable Value Objects. This is acceptable but in that case these Value Objects should never be shared.

Rule 2

Value Objects should almost always be immutable.

Whole value

As Evans theorized DDD and talked about Value Objects, one often tends to think he also invented them. But the origin of the Value Object appears to be the Whole Value, introduced by Ward Cunningham3

In short, a Value Object is either complete, with all its attributes, or it is not. In the color examples above, it would not make sense to leave out one of the channels, and even if we were to make the $alpha attribute optional, it should never be null, as it would no longer be a valid color.

All or nothing

It is therefore highly recommended to instantiate a Value Object once with all its attributes, as we did in the examples above. Setting all the readonly properties in the constructor makes it impossible to end up in an invalid state.

Now, in some cases this is complicated. For more complex objects, we might need to convert data or extract it from a database, use a webservice or parse a file to gather the required information to inject into the Value Object or use, for instance, a Strategy Patttern to calculate the value of the attributes we want to inject into the Value Object.
In short, there are many use cases where it is not that simple to instantiate the object on the fly.

In those cases it is recommended to use a factory, either within the Value Object itself, or, when it gets really complex, in a dedicated class.

<?php
class Color
{
    // ...

    public static function ofHexColor(string $hexColor, float $opacity): self
    {
        $hexColor = ltrim($hexColor, '#');
        $parts = match(strlen($hexColor)) {
            3 => [
                str_repeat(substr($hexColor, 0, 1), 2),
                str_repeat(substr($hexColor, 1, 1), 2),
                str_repeat(substr($hexColor, 2, 1), 2),
            ],
            6 => [
                substr($hexColor, 0, 2),
                substr($hexColor, 2, 2),
                substr($hexColor, 4, 2),
            ],
            default => throw new InvalidColor(
                        sprintf('%s is not a valid CSS color', $hexColor))
        };

        return new self(hexdec($parts[0]), hexdec($parts[1]), 
                        hexdec($parts[2]), $opacity);
    }
}

var_dump(Color::ofHexColor("#f00", 1));

//  Color Object
//  (
//      [red] => 255
//      [green] => 0
//      [blue] => 0
//      [alpha] => 1
//  )

These factories may also be interesting to add additional static constructors to the Value Objects. For instance, in our color example, we might need to be able to instantiate an object with other color notations:

<?php
class Color
{
    // ...

    public static function ofHsvColor(
        int $hue, int $saturation, int $value, float $opacity): self
    {
        // convert, validate, initiate here
    }

    public static function ofCmykColor(
        int $cyan, int $magenta, int $yellow, int $black, float $opacity): self
    {
        // convert, validate, initiate here
    }
}

By the way, these static constructor methods allow for more explicit and more expressive method names, which is always useful, even when not following the DDD principles. It is therefore very common to define a private constructors and implement one or more static methods to initiate the Value Object, even if there is only one:

<?php
class Color
{
    private function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue,
        public readonly float $alpha,
    ) {
    }

    public static function ofRgbaColor(
        int $red, int $green, int $blue, float $alpha = 1.0): self
    {
        // validation
        return new self($red, $green, $blue, $alpha);
    }
}

Evolvable objects

An alternative to factory methods is to use an evolvable object:

<?php
class Color
{
    public function __construct(
        public readonly int $red = 0,
        public readonly int $blue = 0,
        public readonly int $green = 0,
        public readonly float $alpha = 1,
    ) {
    }

    public function withRed(int $red): self
    {
        return new self($red, $this->blue, $this->green, $this->alpha);
    }

    public function withBlue(int $blue): self
    {
        return new self($this->red, $blue, $this->green, $this->alpha);
    }

    // etc with Green & Alpha
}

$color = new Color();
var_dump($color);
//  Color Object
//  (
//      [red] => 0
//      [green] => 0
//      [blue] => 0
//      [alpha] => 1
//  )

$otherColor = $color->withRed(255);
var_dump($otherColor);
//  Color Object
//  (
//      [red] => 255
//      [green] => 0
//      [blue] => 0
//      [alpha] => 1
//  )

As one can see in this very simple example, one can chain a series of more or less complex methods, each one generating a new valid Value Object, until we end up with the final Value Object we aimed for. Please note that in order to get there, we did take care to define valid default values for the Color's four channels.

Rule 3

Value Objects are whole objects and should be complete.

Replaceable

If a Value Object is immutable (see above), one can not simply change its value, or the value of its attributes. That seems pretty obvious.

This means that when we need to change the represented value, we should completely replace it with a new Value Object. This is very easy to understand when using plain scalars:

<?php
$firstname = 'Eric';
$firstname = 'John';

In the above two lines, Eric did not become John, but the value of $firstname, that was initially set to Eric was replaced with the value John (any resemblance with reality is mere coincidence :poke PHPUgly).

The same goes for Value Objects:

<?php
$firstname = new Firstname('Eric');
$firstname = new Firstname('John');

// and not
$firstname->value = 'John';

Rule 4

Referenced Value Objects are not updated but replaced with new instances.

Side-effect-free

Now this is a good practice that comes from functional programming and is tightly related with the immutability and replacement principles explained above.

Side-effect-free means that any operation on an object, in this case, a Value Object, should not alter the object's state.

Do check out Larry Garfield's book if you would like to explore FP further.
Larry Garfield - Thinking functional in PHP

<?php
class Total
{
    public function __construct(
        public readonly int $value,
    ) {
    }

    public function add(int $valueToAdd): self
    {
        return new self($this->value + $valueToAdd);
    }
}

$originalTotal = new Total(2);
$newTotal = $originalTotal;
$newTotal = $newTotal->add(3);

echo $originalTotal->value;    // 2
echo $newTotal->value;      // 3

In the example above, the value of $originalTotal does not change, even when a method is called on it and a new Value Object, $newTotal, is created with a different value.

Rule 5

Value Objects remain unaffected by the operations that are applied upon them.

OK great... now what?

Now that we more or less understand what a Value Object is and have seen a series of very simple PHP examples, let us have a closer look at the benefits of using them.

Value objects are valid objects

In the examples above, we mostly focused on the properties of the Value Objects and barely touched one of the biggest advantages of them, being that, when implemented properly, Value Objects are always valid objects.

<?php
class PhpArchEmailAddress
{
    public readonly string $value;

    public function __construct(string $value) 
    {
        if(preg_match('/.*@phparch\.com/m', $value) !== 1) {
            throw new InvalidPhpArchEmailAddress(
                sprintf('<%s> is not a valid email address.', $value));
        }

        if(preg_match('/ben\.ramsey@phparch\.com/m', $value) > 0) {
            throw new ExasperatedEric("Ben? Ben Ramsey? Are you kidding me?");
            // private joke for the PHPUgly Community
        }

        $this->value = $value;
    }
}

Whether directly in the constructor, or in a factory, one can apply all the required validation and filter rules before instantiating the Value Object. As a result, if the object can be initiated with the passed in attributes, it will be valid. More, it is auto-validated!

If one were to use a basic string to deal with a such an email address, one would have to check its validity every time it is used. With immutable Value Objects, one no longer needs to verify anything. Its sole existence proves that the value(s) it contains are valid. This does clean up our code drastically and makes it much easier to maintain and to read.

Side note: when the validation rules get a little more complicated, it is recommended to move them to a dedicated class that will be easier to maintain and, more importantly, to test.

So, yes, Value Objects are ALWAYS valid!

Types

Since PHP 7.4, we have been blessed with proper typed properties in PHP and we can now benefit from all the advantages other strongly typed languages offer in terms of performance and security.

Value Objects are perfect to extend our favorite programming language with rich and expressive types.

Compare both of these examples:

<?php
class PhpArchNewEditionNotifier
{
    // with a string
    public string $senderEmailAddress;

    // ...
}

and

<?php
class PhpArchNewEditionNotifier
{
    // with a Value Object
    public PhpArchEmailAddress $senderEmailAddress;

    // ...
}

Next to being much more efficient, remember one does not need to write/copy-paste validating lines every time the value is used, the second example is much more expressive as it clearly indicates what the $senderEmailAddress should be.

Specialized & Rich Models

Value Objects address one particular concern and, next to the values, can encapsulate rich and context bound business logic.

This means one will not bundle things together if they don't belong to the same context.
Instead of this:

<?php
class Price
{
    public function __construct(
        public readonly float $price,
        public readonly string $currency,
        public readonly float $vatRate,
        public readonly ?float $discountRate = null,
        public readonly ?float $discountAmount = null,
    ) {
    }
}

one could move attributes that belong to the same context into dedicated Value Objects.
For instance:

<?php
class Price
{
    public function __construct(
        public readonly Money $price,
        public readonly VatRate $vatRate,
        public readonly Discount $discount,
    ) {
    }
}

In the example above, we are using the Money for PHP library (see https://www.moneyphp.org/). Next to dealing with the well known float issues when dealing with money, it is also a remarkable example of a Value Object.

In short, it more or less functions like this (not in PHP8.1):

<?php
class Currency
{
    public function __construct(
        private string $code,
    )
}

class Money
{
    public function __construct(
        private int|string $amount,
        private Currency $currency,
    ) {
    }
}

$amounts are in cents and are, in the end, converted to a string in order to be able to store values that are greater than the system's integer limit (PHP_INT_MAX). Check out Money's source code to get a better understanding of the $amount attribute.

The VatRate is a rather simple Value Object:

<?php
class VatRate
{
    public function __construct(
        public readonly float $rate,
    ) {
    }
    // ...
}

Interestingly enough, one could add a nice method to the VatRate Value Object to calculate the VAT and to apply the VAT, making the model richer:

<?php
class VatRate
{
    // ...

    public function calculateVat(Money $money): Money
    {
        return $money->multiply($this->rate);
    }

    public function applyVat(Money $money): Money
    {
        return $money->add($this->calculateVat($money));
    }

    // Money being immutable, the add() & multiply()
    // methods will return new instances of Money
}

Last but not least, the Discount Value Object. One may have been wondering why, in the first example, there were $discountRate and $discountAmount, both nullable. That is because the system should be able to deal with rates and fixed amounts for the discount.

This probably is a smarter approach:

<?php
interface Discount
{
    public function asRate(Money $money): float;

    public function asMoney(Money $money): Money;

    public function applyDiscount(Money $money): Money;
}

class RateDiscount implements Discount
{
    public function __construct(
        public readonly float $rate,
    )

    public function asRate(Money $money): float
    {
        return $this->rate;
    }

    public function asMoney(Money $money): Money
    {
        return $money->multiply($this->rate);
    }

    public function applyDiscount(Money $money): Money
    {
        return $money->subtract($this->asMoney());
    }
}

class FixedAmountDiscount implements Discount
{
    public function __construct(
        public readonly Money $amount
    ) {
    }

    public function asRate(Money $money): float
    {
        return $this->amount->getAmount() / $money->getAmount();
    }

    public function asMoney(Money $money): Money
    {
        return $this->amount;
    }

    public function applyDiscount(Money $money): Money
    {
        return $money->subtract($this->asMoney());
    }
}

Finally, one can easily figure out the total price:

<?php
class Price
{
    // ...

    public function total(): Money
    {
        return  $this->vatRate->applyVat(
                    $this->discount->applyDiscount(
                        $this->price
                    )
                );
    }
}

This is just a very simple example, but it does illustrate that Value Objects are very specialized objects and that they can encapsulate very context bound business logic.

Testing

As we are testing the code we write, or, better, write the code that addresses a written test (see TDD), testing just got much more straightforward.

The reason is pretty obvious. If we were, for instance, to use a regular email address (as string) as an attribute in multiple functions in multiple classes, we would need to write tests with valid and invalid email addresses for each one of them. With an EmailAddress Value Object, we only need to write those tests once and will no longer have to worry about it anywhere else.
As mentioned above, if a Value Object can be instantiated it must be valid.

Last but not least, there is no need to mock Value Objects.

Cognitive load

As the usage of Value Objects encourages you to encapsulate the context bound business logic along with it, one will, in the end write more classes, but much smaller ones.

As a result, just like in DDD, where the business logic is split over multiple insulated contexts, the cognitive load required to understand what a given object does, how it works, and how to interact with it gets reduced... Even more now one knows that it can not change all of a sudden and that it will de facto always be valid.

Caching

Once more, because of their immutability, Value Objects become perfect for caching, even for a very long period of time.

This can be done either with a application wide caching mechanism:

<?php
$psr6CachePool = new Cache();

if(!$psr6CachePool->hasItem(PhpArchEmailAddress::class . $emailAddressAsString)) {
    $psr6CachePool->save(
        $psr6CachePool->getItem(PhpArchEmailAddress::class . $emailAddressAsString)
                    ->setValue(new PhpArchEmailAddress($emailAddressAsString))
    );
}
$emailAddress = $psr6CachePool
                    ->getItem(PhpArchEmailAddress::class . $emailAddressAsString)
                    ->get();

or through memoization:

<?php
class ComplicatedMathematicalValueObject
{
    private ?float $result = null;

    public function __construct(
        public readonly float $val1, 
        public readonly float $val2,
    ) {
    }

    public function complicatedOperation(): float
    {
        if(is_null($this->result)) {
            $this->result = // very long operation goes here
        }
        return $this->result;
    }
}

Bear in mind that when using memoization, one loses the ability to use the == operator to compare two Value Objects as for the == operator, the Value Object before and after memoization will no longer be equal.

<?php
$var1 = new ComplicatedMathematicalValueObject(1.2, 1.3);
$var2 = new ComplicatedMathematicalValueObject(1.2, 1.3);

var_dump($var1 == $var2);   // true

$var1->complicatedOperation();

var_dump($var1 == $var2);   // false

It is therefore very common to add an isEqual() method to the Value Objects to deal with this.

<?php
class ComplicatedMathematicalValueObject
{
    // ...

    public function isEqual(ComplicatedMathematicalValueObject $other): bool
    {
        return $this->val1 === $other->val1 && $this->val2 == $other->val2;
    }
}

$var1 = new ComplicatedMathematicalValueObject(1.2, 1.3);
$var2 = new ComplicatedMathematicalValueObject(1.2, 1.3);

var_dump($var1->isEqual($var2));    // true

$var1->complicatedOperation();

var_dump($var1->isEqual($var2));    // true

By the way, this isEqual() method will also deal with issues with inheritance:

<?php
class MustacheColor extends Color
{

}

$colorA = new Color(255, 255, 255, 1);
$colorB = new MustacheColor(255, 255, 255, 1);

var_dump($colorA == $colorB); // false

This makes sense, the == operator also expects the objects to be of the exact same type. Sadly, it is not possible, yet, to overload PHP's operators.

Luckily, the isEqual() method will deal with this:

<?php
class Color
{
    // ...
    public function isEqual(Color $otherColor): bool
    {
        return $this->red === $otherColor->red
            && $this->green === $otherColor->green
            && $this->blue === $otherColor->blue
            && $this->alpha === $otherColor->alpha;
    }
}

$colorA = new Color(255, 255, 255, 1);
$colorB = new MustacheColor(255, 255, 255, 1);

var_dump($colorA->isEqual($colorB)); // true

Remember that we care about the values here. It is obvious that a MustacheColor and a Color are not identical, yet their values are.

Caveats

And what about DTOs?

As Matthias Noback wrote in a blog post lately4, DTOs and Value Objects are not the same.

First of all, they have a different purpose. DTO, or Data Transfer Objects, are used on the boundaries of your application, either when data enters it, or when it leaves it. Value Objects, on the other hand are, mostly, used within the application and even more often within its business layer.

Secondly, even though both DTOs and Value Objects define a structure for data, DTOs do not need to be complete as it is required for a Value Object (see Whole Value above). DTO's properties can be nullable, and it is not the DTO's responsibility to validate or only accept valid data or state.

Thirdly, as indicated above, Value Objects ought to be specialized, only deal with a specific concern and, eventually, include context bound business logic. DTOs, on the other hand, do not require to be limited to a specific scope. They need to make data interchangeable, in and out of an application, and should be formatted according to the usage that will be made of it. Also, DTOs will barely ever include any complex business logic.

Persistence

One of the biggest challenges when working with Value Objects clearly is persistence, ie storing it in a database or wherever. The complication mostly comes from the absence of an identifier. This mind shift does require a little effort as we are kind of used to mirror our database design with our code, or the other way around.

This is another principle that comes from DDD, where the code that is used to express the business layer is to be completely decoupled from the persistence layer, for instance, the database. PHP Value Objects are only useful in a PHP context, not in a database... at least not as such. So it makes perfect sense to break the sacred rule of one model/one table and one property/one column!

An unpopular point of view, maybe: the database is the place where one stores data, NOT where one consumes it. If one needs to use the data outside of the application, either one ought to use the application to fetch it -- for instance, through a dedicated API that uses the application's business logic -- or build some kind of data warehouse where the data is transformed and can be optimized for that specific task.

There are multiple ways to store these Value Objects in the database though. There will always be a suitable solution:

Along with the encapsulating entity

Remember, and this is important, your database does not have to be an exact reflection of your code (and vice versa).

For example,

<?php
// Entity
class Book
{
    public readonly Uuid $uid;
    private Author $author;
    private Title $title;
    private ?Color $coverColor;
    // ...
}

could simply be stored in a table that would look like this:

create table book
(
    uid                 varchar(36)  not null,
    author_uid          varchar(36)  not null,
    title               varchar(255) not null,
    cover_color_rgba    char(8)      null,
    -- ...
    foreign key (author_uid) references author(uid)
    -- ...
);

The Author being another entity, with an identity, the book table contains a foreign key to the author table.

The Title Value Object is directly stored as string.

And in the above example, the Color is stored in its RGBA notation (and this is explicitly indicated) even though the original Color Value Object uses three ints and a float. It is shorter and easier to store in a single field this way.

Depending on the chosen technology for the communication with the database (raw SQL, PDO, ORM, REST...), this communication layer will be responsible for the conversion of the data in the database into valid PHP objects and the other way around. It is highly recommended to have a close look at the Repository pattern to deal with this.

Denormalization

Much has been said about how to avoid repetition in code and why we should do so. Maybe a little to much. This is also often enforced in databases where one tends to use foreign keys to reference data that is reused elsewhere.

Yet, in the case of Value Objects, especially for small ones, denormalization probably is one of the most interesting approaches. Denormalization is the exact opposite of "avoiding repetition", except one does it on purpose. Please note that unlike one may think, if done properly, this might actually improve the database's performance as additional joins are no longer required.

Consider this code

<?php
// Entity
class Book
{
    // ...
    private ?Color $coverColor;
    private ?Money $price;
    // ...
}

If one were to mimic the code's structure and did not want the RGBA Color notation, this would result in database tables that could look more or less like these:

create table color
(
    id                  int auto_increment primary key,
    color_red           int          not null,
    color_green         int          not null,
    color_blue          int          not null,
    color_alpha         float        not null,
)

create table price
(
    id                  int auto_increment primary key,
    amount              char(10)     null,
    price_currency      char(3)      null,
)

create table book
(
    -- ...
    cover_color_id      int         null,
    price_id            int         null,
    -- ...
    foreign key (cover_color_id) references color(id)
    foreign key (price_id) references price(id)
    -- ...
)

Color and Price would have an identity and foreign keys would be created to reference the $bookCoverColor and $price. If we were to add another concept with a price or a color, we would be able to use the same color and price tables. Multiple joins would be required to fetch all the book's attributes.

But it could also be stored in a denormalized way, in one single database table that would look like this:

create table book
(
    -- ...
    cover_color_red           int          null,
    cover_color_green         int          null,
    cover_color_blue          int          null,
    cover_color_alpha         float        null,
    price_amount        int          null,
    price_currency      char(3)      null,
    -- ...
);

As one can see in this example, the Color Value Object is stored directly in four fields, one per channel, in the book table. Depending on database usage and personal preferences, one could choose this version or the one in the previous code listing using the RGBA notation.

The Money $price Value Object is stored into two distinct columns, one for the amount another one for the currency.

Side-note: even though the $amount in a Money Value Object really is a string, in the example, it is stored as an int. Thing is, a book's price will never exceed PHP_INT_MAX and the data department will be happy the data is immediately usable and will not need to get casted.

This does mean however that other concepts with a color and/or price would need to implement these columns as well. But is that really a problem? Not really...

Serialization

One of the easiest ways is to serialize the data and store it in one single field in the database, preferably in JSON format (not with PHP's serialize() function).

Remember that, these days, most of the RDBMS are able to deal directly with JSON. So if one really needs to access the data in the database directly, even though it will be a little slower, it still is possible.

A Color field would look like:

{"red" : 255, "green" : 0, "blue" : 0, "alpha" : 1}

Readable, exploitable, single field... Definitely an option to bear in mind.

As an entity

Last but not least, in some cases it does make sense to consider a Value Object as an entity when storing it (and only when storing it). This, however, should only be considered when other options are not feasible.

There are multiple options here, all of them are right and really depend on how much one wants to decouple the persistence layer from the Value Objects.

First option, the easiest one, is to autogenerate a unique identifier when instantiating the Values Object. This property should be private and not accessible via the object's public API (when using an ORM like Doctrine, you actually don't need any setter or getter methods).

<?php
class Color
{
    private int $id;

    public function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue,
        public readonly float $alpha,
    ) {
    }
}

Second option is the one used in DDD-world and makes use of a surrogate identifier. This is achieved by using a parent class with a private or protected property

<?php
abstract class ValueObjectWithId
{
    private int $id;
}

class Color extends ValueObjectWithId
{
    public function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue,
        public readonly float $alpha,
    ) {
    }
}

Obviously, this could also be achieved with at trait:

<?php
trait ValueObjectWithId
{
    private int $id;
}

class Color
{
    use ValueObjectWithId;

    // ...
}

In all three cases the generation of the id would be delegated to the database. ORM systems like Doctrine are able to hydrate the private properties but this could also be achieved with Reflection or by binding a closure to the class (see Marco Pivetta's, aka @ocramius, blog post here: http://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/).

This approach does require more SQL queries though as, when saving the Value Object to the database, one will have to check if an entry with the same properties already exists, inject the private id that will be used later on in the encapsulating entities or Value Objects.

Dedicated Persistence Types

ORMs like Doctrine and Eloquent allow us to create dedicated adapters that take care of the conversion of DataTypes, in this case the Value Objects, from the application's PHP into the database and the other way around.

One still has to decide which of the above strategies is the most suited... But once implemented, these auto-magic transformers come in quite handy.

Enums

As PHP 8.1 introduced the long awaited native enums, it seems useful to add a little note on those. It seems very obvious to store a PHP Enum as an Enum in a database... Thing is, this may not be the best move.

Back in 2011, yes, that's a long time ago, Chris Komlenic wrote a little blog post listing a series of issues with the MySQL Enum type5. As MySQL still is one of the most popular RDBMS, this list of objections remains pretty much valid. In PostgreSQL, Enums are real reusable data types, so part of the objections are less valid for that RDBMS.

Granted, database Enums are easy to use, but they come at a cost and, in the end, offer only very negligible benefits on optimization (an in depth analysis of this would be out of scope of the current article).

In the end, and especially when working with an ORM, it is really not complicated to use one of the strategies mentioned above and avoid the "evil" native database enum type.

Some Interesting Value Objects

We already mentioned the DateTimeImmutable object and had a look at the Money Value Object in the examples above and also mentioned its benefits over using floats to deal with monetary values.

Here are some more Value Objects that might be of interest:

UUID

Ben Ramsey created PHP's best implementation of Uuid (https://uuid.ramsey.dev/en/stable/index.html).
By the way, Ben just added version 7 of the Uuid specification. Make sure to check it out.

Not NULL

In his brilliant blog post "Much ado about null", Larry Garfield shows a series of very interesting alternatives to the usage of null. Check out the Maybe [Monad] Value Object, and even more his Result proposal at the end of the post (https://peakd.com/hive-168588/@crell/much-ado-about-null).

Symfony/String

The Symfony/String component is another very nice implementation of a "big" Value Object that handles strings as immutables and provide rich functionality (https://symfony.com/doc/current/components/string.html).

Conclusion

We now know what a Value Object is, that it is one fundamental tool when working in Domain Driven Design, but that it definitely is a very interesting approach anywhere. We've briefly discussed Value Object's characteristics, its benefits and ways to use and store them.

Domain Driven Design is gaining a lot of traction within the PHP community, which only shows our favorite programming language has matured a lot over the last years, and chances are Value Objects, and other concepts from DDD, will become more and more used, even when not working on a DDD project.

Another building block to bring value to our code...

References


Comments