Value Objects in PHP 8: Building a better code
Tags: php, designpatterns, programming, tutorial
Published: 2023-11-30 04:08:04

## Table of contents
- [Introduction](#introduction)
- [Problems with primitives](#problems_with_primitives)
- [Class as types](#class_as_types)
- [Key Qualities of Value Objects](#key_qualities)
- [Wrapping It Up](#wrapping_it_up)

## <a name="introduction"></a>Introduction
In the world of coding, keeping our code clean and strong is a big deal. 

The Value Object pattern has the potential to significantly enhance your code quality, making it more robust and maintainable. 

In this article, I will explain how to implement the pattern and how this will add some "sugar" to your code, using the latest features introduced by PHP 8.1 and PHP 8.2.

## <a name="problems_with_primitives"></a>Problems with primitives
Before we jump into Value Objects, let's talk about the issues with basic data types. Here are three common problems:

### 1. Invalid values
Simple data types don't have built-in checks to ensure the data is valid. This can lead to unexpected issues in our code.

An age could be represented by an integer, but of course, cannot be negatives or more than 120 (more or less). Maybe in our domain makes sense that age should be more or equal than 18 years old. 

An email could be represented by a string, but it's not. It is a subset of all possible strings, and several checks are necessary to ensure is valid.

We possibly have a lot of different places in our code that use those values, but we can trust no one, and we had to ensure that our data is consistent. Consequently, we must validate the data passed as an argument every time.

This leads to a validation logic duplication issue. Each of these duplicated logic could potentially differ from each other, leading to inconsistencies.

```php
function logic1(int $age): void 
{
    ($age >= 18) or throw InvalidAge::adultRequired($age);

    // Do stuff
}

function logic2(int $age): void 
{
    ($age >= 0) or throw InvalidAge::lessThanZero($age);
    ($age <= 120) or throw InvalidAge::matusalem($age);

    // Do stuff
}
```

Using Value Objects should do the trick. It will simplify a lot your code and also ensure consistency in your data.

```php
readonly final class Age 
{
     public function __construct(public int $value)
     {
          ($value >= 18) or throw InvalidAge::adultRequired($value);
          ($value <= 120) or throw InvalidAge::matusalem($value);
     }
}
```

```php
function logic1(Age $age): void 
{
    // Do stuff
}

function logic2(Age $age): void 
{
    // Do stuff
}
```

In this way, you are sure that if an instance of Age exists, it is valid and consistent, everywhere in your code, without checking every time.

### 2. Mixing Up Arguments
When dealing with functions that take similar types of data, it's easy to mix up the order of arguments. This can cause hard-to-spot bugs.

```php
function logic1(string $name, string $surname): void 
{
    // Logic error, 
    // $name is switched with $surname, unintentionally
    logic2($name, $surname);
}

function logic2(string $surname, string $name): void {
    // Do stuff
}
```

This type of bug is particularly tricky, and currently, there isn't a built-in check that could help us prevent it. 

There are only two ways to solve this issue:
* **Using Value Objects**: The interpreter itself or static analysis tools can easily recognize when there's a type mismatch.

```php
function logic1(Name $name, Surname $surname): void 
{
    // Static analysis error
    // Expected Surname, found Name
    logic2($name, $surname);
}

function logic2(Surname $surname, Name $name): void {
    // Do stuff
}
```

* **Using named arguments:** Introduced in PHP 8.0. In this way, order doesn't matter.
```php
function logic1(string $name, string $surname): void 
{
    logic2(name: $name, surname: $surname);
}
```

 
### 3. Accidental Changes

Simple data types can be changed without us realizing it. When we pass them to a function, that function might accidentally modify the original data.

```php
function logic1(int &$age): void 
{
    if ($age = 42) { // BUGS alert
        echo "That's the answer\n";
    }

    echo "Your age is $age\n"; // It will print always 42
}
```

Given that is rare to pass arguments by reference using `&`, this example has got not one but two bugs:
* `$age = 42` is an assignment, not a comparison. It overwrites the current value without us noticing it and everything that uses that value after will be affected by this bug.
* Changing a value passed by reference means that the variable outside the call will be changed as well. This could be intentional, but sometimes it's not.

Using Value Objects will solve this issue because they grant immutability.

```php
final readonly class Age
{
    public function __construct(public int $value)
    { // validation }
}

function logic1(Age $age): void 
{
    // Interpreter error
    // cannot write a readonly property
    if ($age->value = 42) { 
        echo "That's the answer\n";
    }

    echo "Your age is $age\n"; 
}
```

## <a name="class_as_types"></a>Classes as Types
Value Objects fix these problems by treating classes as types. Unlike simple data types, Value Objects wrap their data in a class. This helps us control and validate our data better.

## <a name="key_qualities"></a>Key Qualities of Value Objects
To make the most of Value Objects, we need to focus on a few important things:

### 1. Can't Change It (Immutability)
Value Objects should stay the same once we create them. This helps us avoid unexpected changes. In the past, prior to PHP 8.1, that was achieved by having private/protected properties and getters only. Setters were prohibited.

If there is a reason why the inner data has to change, then a new instance will be created, without changing the current instance. 

PHP 8.1 simplified a lot introducing `readonly` properties and property promotion.

```php
class Age // PHP 8.1
{
    public function __construct(public readonly int $value)
    {
        ($value >= 18) or throw InvalidAge::adultRequired($value);
        ($value <= 120) or throw InvalidAge::matusalem($value);
    }
}
```

Prior to PHP 8.1
```php

class Age // PHP < 8.1
{
   private int $value;

    public function __construct(int $value)
    {
        ($value >= 18) or throw InvalidAge::adultRequired($value);
        ($value <= 120) or throw InvalidAge::matusalem($value);

        $this->value = $value;
    }

    public function value():int { return $this->value; }
}
```

The following example shows how to deal with changes in the value object. 
As you can see, no instance property is changed. Instead, a new instance was created. 

```php
final readonly class Money // PHP 8.2
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {
        ($amount > 0) or throw InvalidMoney::cannotBeZeroOrLess($amount);
    }

    public function sum (Money $money): Money 
    {
        if ($money->currency !== $this->currency) {
            throw InvalidMoney::cannotSumPearsWithApples($this->currency, $money->currency);
        }
        
        $newAmount = $this->amount + $money->amount;

        return new Money($newAmount, $this->currency);
    }
}
```

### 2. Easy to Compare (Comparability)
Making Value Objects comparable means we can easily check if they are the same or different. This comes in handy when sorting or searching.

```php
// Money
public function equals(Money $money): bool 
{
    return $this->amount === $money->amount
        && $this->currency === $money->currency;   
}

// code
$thousandYen = new Money(1000, Currency::YEN);
$thousandEuro = new Money(1000, Currency::EURO);

$thousandYen->equals($thousandEuro); // false
```

Because of course, ¥1000 is not the same as €1000.

### 3. Always Good Data (Consistency)
A Value Object should always represent something valid. By checking and validating inside the object, we make sure it's always in good shape.

This means that the validation should be done inside the constructor to ensure that if the instance exists, then it's valid. Always!

> **WARNING!**
> Be careful with deserializers: sometimes they build the object without calling the constructor.

An interesting approach could be having a validate method, called inside the constructor and after one of this kind of deserializing process.

```php
public function __construct(public string $value)
{
    $this->validate();
}

private function validate(): void 
{
    // Do the validation stuff
}
```

For example, using [Serde PHP](https://github.com/Crell/Serde) deserializer, you must add the attribute `#[PostLoad]` to make it call after the instantiation.

```php
#[PostLoad]
private function validate(): void 
{
    // Do the validation stuff
}
```

This is because most of them use [this reflection method](https://www.php.net/manual/en/reflectionclass.newinstancewithoutconstructor.php) under the hood

```php
public ReflectionClass::newInstanceWithoutConstructor(): object
```

### 4. Easy to debug (Debuggability)
It's a good practice to equip Value Objects with an easy way to debug themself. In the case of a simple value object, a `__toString` method shall do the job. Otherwise, in the case of a composite value object (with a lot of properties and inner other value objects) a `toArray` is suggested. Or else, a serializer might be used.

```php
final readonly class Name
{ 
    public function __construct(public string $value) {}

    public function __toString(): string 
    { 
        return $this->value; 
    }
}
```

```php
final readonly class Surname
{ 
    public function __construct(public string $value) {}

    public function __toString(): string 
    { 
        return $this->value; 
    }
}
```

```php
final readonly class Person
{ 
    public function __construct(
        public Name $name, 
        public Surname $surname
    ){}

    public function __toString(): string
    {
        return "{$this->name} {$this->surname}";
    }

    public function toArray(): array 
    {
        return [
            'name' => (string)$this->name,
            'surname' => (string)$this->surname
        ];
    }
}
```

## <a name="wrapping_it_up"></a>Wrapping It Up
In conclusion, embracing the Value Object pattern in PHP 8.2 significantly enhances code quality, making it more robust and maintainable. By treating classes as types, focusing on immutability and always-valid data, and leveraging features introduced in PHP 8.1 and 8.2, developers can create more stable and resilient applications. Adding Value Objects to your coding toolkit not only improves the look of your code but also simplifies the development process and sets the stage for a more scalable and error-resistant codebase.