Using value-objects in Laravel Models
Tags: laravel, php, programming, tutorial
Published: 2021-07-26 23:41:44

---
title: Using value-objects in Laravel Models
published: true
description: Make your data more precise, more resilient, and bring it closer to the business domain.
tags: ["laravel","php","programming","tutorial"]
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m6tsvq1pt2255b86c5af.png
 
---

(image credits [the Joomla Community](https://magazine.joomla.org/all-issues/july-2014/the-value-of-value-objects))


## Value Objects

> A value object is a small object that represents a simple entity whose equality is not based on identity: i.e. two value objects are equal when they have the same value, not necessarily being the same object.
>
> Examples of value objects are objects representing an amount of money or a date range.
>
> Value objects should be immutable: this is required for the implicit contract that two value objects created equal, should remain equal. It is also useful for value objects to be immutable, as client code cannot put the value object in an invalid state or introduce buggy behaviour after instantiation.

You may read the full definition on [Wikipedia](https://en.wikipedia.org/wiki/Value_object).

In short: **Value Objects** are objects that

- Hold a value
- Are (generally) immutables
- Carry more context than native types

## Use Case

Consider the following tables:

```text
┌───────────────────┐       ┌────────────────────┐
│ invoices          │       │ invoice_line_items │
├───────────────────┤1,1    ├────────────────────┤
│ id (int, primary) │◄──┐   │ id (int, primary)  │
│ customer_id (int) │   └───┤ invoice_id (int)   │
│ status (string)   │    0,N│ label (string)     │
└───────────────────┘       │ quantity (int)     │
                            │ unit_price (int)   │
                            └────────────────────┘
```
(made with [asciiflow](asciiflow.com))

We can immediately identify the following rules that apply here:

- `invoices.status` values are constrained within a list of possible values (Sent, Paid, Void, etc.)
- `invoice_line_items.quantity` and `invoice_line_items.unit_price` cannot be negative

While it *could* make sense to host the code responsible for checking the data integrity inside the models - in `Invoice::setStatusAttribute` and `InvoiceLineItem::setQuantityAttribute` for instance - I'm going to present you a **more robust and elegant way to implement those rules**.

## Introducing Value Objects

Let's start with `App\Models\InvoiceStatus` (we're going to host the value objects in the same namespace as the models, more on that later.)

```php
namespace App\Models;

final class InvoiceStatus
{
    private function __construct(
        private string $value
    ) {}

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

    public function equals(self $status): bool
    {
        return $this->value == $status->value;
    }

    public static function fromString(string $status): self
    {
        return match ($status) {
            'draft' => self::draft(),
            'sent' => self::sent(),
            'paid' => self::paid(),
            'overdue' => self::overdue(),
            'void' => self::void(),
            'writeOff' => self::writeOff(),
            default: throw new \InvalidArgumentException("Invalid status '{$status}'");
        };
    }

    public static function draft(): self
    {
        /* You’ve created an incomplete invoice and it hasn’t been sent to the customer. */
        return new self('draft');
    }

    public static function sent(): self
    {
        /* Invoice has been sent to the customer. */
        return new self('sent');
    }

    public static function paid(): self
    {
        /* Invoice has been paid by the customer. */
        return new self('paid');
    }

    public static function overdue(): self
    {
        /* Invoice has past the payment date and the customer hasn't paid yet. */
        return new self('overdue');
    }

    public static function void(): self
    {
        /* You will void an invoice if it has been raised incorrectly. Customers cannot pay for a voided invoice. */
        return new self('void');
    }

    public static function writeOff(): self
    {
        /* You can Write Off an invoice only when you're sure that the amount the customer owes is uncollectible. */
        return new self('write-off');
    }
}
```

You may have noticed the constructor is private, and the class is final. This ensures that nobody can derive it to create new values or instances with an invalid state, constraining the possible values to strictly the list provided by the static methods.

Trust me; this will help **A LOT** down the road because **the values can only be changed at a single place in the app**. No more search for incorrect constants & values with CTRL+SHIFT+F!

## Value Object Casting

Turns out Laravel has a native solution to perform [value object casting](https://laravel.com/docs/8.x/eloquent-mutators#value-object-casting) in models! All we need to do is to create a new custom cast with:

```bash
php artisan make:cast InvoiceStatusCast
```

And then fill it with:

```php
namespace App\Casts;

use App\Models\InvoiceStatus;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class InvoicestatusCast extends CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        if (is_null($value)) {
            return null;
        }

        return InvoiceStatus::fromString($value);
    }

    public function set($model, $key, $value, $attributes)
    {
        if (is_string($value)) {
            $value = InvoiceStatus::fromString($value);
        }

        if (! $value instanceof InvoiceStatus) {
            throw new \InvalidArgumentException(
                "The given value is not an InvoiceStatus instance",
            );
        }

        return $value;
    }
}
```

## The Model

Now we've defined our value object and our attribute caster, let's make `Invoice` use it.

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Casts\InvoiceStatusCast;

class Invoice extends Model
{
    protected $fillable = [
        'status',
    ];

    protected $attributes = [
        /* default attributes values */
        'status' => InvoiceStatus::draft(),
    ];

    protected $casts = [
        'status' => InvoiceStatusCast::class,
    ];

    public function lineItems()
    {
        return $this->hasMany(InvoiceLineItem::class);
    }
}
```

## More Value Objects

Now let's quickly do the two others:

```php
namespace App\Models;

class UnsignedInteger
{
    private $value;

    public function __construct(int $value)
    {
        if ($value <= 0) {
            throw new \UnexpectedValueException(static::class . " value cannot be lower than 1");
        }

        $this->value = $value;
    }

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

final class InvoiceLineItemQuantity extends UnsignedInteger
{
    public function add(self $quantity): self
    {
        return new self($this->value() + $quantity->value());
    }

    public function substract(self $quantity): self
    {
        return new self($this->value() - $quantity->value());
    }
}

final class InvoiceLineItemUnitPrice extends UnsignedInteger
{
    public function increase(self $price): self
    {
        return new self($this->value() + $price->value());
    }

    public function decrease(self $price): self
    {
        return new self($this->value() - $price->value());
    }
}
```

You might be tempted to move `InvoiceLineItemQuantity::add` and `InvoiceLineItemUnitPrice::increase` to `UnsignedInteger` for instance, and maybe rename them both to `add` or `sum`, but then you would make it possible to write `$price->add($quantity)` which is a bit silly.

Those methods * look like duplication* but it's accidental. You need to keep them separated in their classes so you can **precisely match the [ubiquitous language](https://martinfowler.com/bliki/UbiquitousLanguage.html#:~:text=Ubiquitous%20Language%20is%20the%20term,language%20between%20developers%20and%20users.&text=He%20also%20stresses%20that%20the,understanding%20of%20the%20domain%20grows.) of your [business domain](https://en.wikipedia.org/wiki/Business_domain#:~:text=A%20business%20domain%20in%20object,the%20business%20model%20being%20implemented.)**.

Use exactly the same logic as above to write the cast classes and then add them to the model:

```php
namespace App\Models;

class InvoiceLineItem extends Model
{
    protected $fillable = [
        'label',
        'quantity',
        'unit_price',
    ];

    protected $casts = [
        'quantity' => InvoiceLineItemQuantityCast::class,
        'unit_price' => InvoiceLineItemUnitPriceCast::class,
    ];

    public function invoice()
    {
        return $this->belongsTo(Invoice::class)->withDefault();
    }
}
```

## Usage

Now we have made a value object that guarantees data integrity **by design**. From your model point of view, this means it is impossible to write something in the database that doesn't match your business logic.

```php
$invoice = tap($customer->invoices()->create(), function ($invoice) {
    $invoice->lineItems()->create([
        'label' => "Dog food"
        'quantity' => new InvoiceLineItemQuantity(3);
        'unit_price' => new InvoiceLineItemUnitPrice(314); // $3.14
    ]);

    $invoice->lineItems()->create([
        'label' => "Cat food"
        'quantity' => new InvoiceLineItemQuantity(5);
        'unit_price' => new InvoiceLineItemUnitPrice(229); // $2.29
    ]);
});

Mail::to($customer->email)->send(new InvoiceAvailable($invoice));

$invoice->update([
    'status' => InvoiceStatus::sent()
]);
```

## Isn't it a little too complex?

The code responsible for handling validation has to go somewhere. In my opinion, it's far better to manipulate values that **validates themselves** rather than having validation code laying around across the whole app. Also, value objects add a lot more context to those values: you need to look at them to understand what they represent and their underlying rules. They are the embodiment of actual business constraints within your code.

I choose to locate my value objects alongside the eloquent models in my apps. I do this for two reasons: I have nowhere else to put them, and, more importantly, it is not rare a value object becomes an actual model. For example, what if my customers want to create custom statuses like *"Needs validation by Mike, the accountant"*.

Well, you would then create an `invoice_statuses` table, right? Then create an InvoiceStatus model object... Wait! You can reuse the existing InvoiceStatus class and implement its methods to use the database instead of static values. This way, you **keep the current code intact**. This is **HUGE!**

## Going further

I highly recommend you take a look at the [excellent spatie/enum](https://github.com/spatie/enum) which does the heavy lifting for you. There is also to the [new PHP8.1 Enum structure](https://stitcher.io/blog/php-81-before-and-after#enums) which will make all the above code even easier to write!

---

As usual, don't forget to like, leave a comment, and follow me on [dev.to](https://dev.to/bdelespierre). It helps me stay motivated to write more articles for you!