What is Value Object Casting in Laravel?

Arie Visser • July 8, 2020

laravel php database

Laravel 7 introduced Custom Eloquent Casts.

Next to primitive types, such as json, it is also possible to cast values that form a compound to objects. This is an effective way to introduce value objects to your Eloquent models, and make your data more reliable.

In this guide we will look into a practical example of value object casting.

What are value objects?

A value object can be seen as an immutable wrapper for one or more primitive types. When creating a value object, all data that is passed to it will be validated in order to improve the reliability of the object.

Some examples of value objects are:

final class Distance
{
    private int $measurement;
    private string $unit;

    // 
}

final class Price
{
    private int $amount;
    private int $precision;
    private string $currency;

    //
}

final class Position
{
    private int $x;
    private int $y;

    //
}

They make up small building blocks, that can be used in any place of your application.

If you want to read more about value objects and their use, I can recommend this article by Martin Fowler and Object Design Style Guide by Matthias Noback.

What is attribute casting?

Attribute casting can be seen as a small transformation layer between your application and the database.

All attributes that need to be transformed after they are retrieved from the database, and / or before they are stored in the database, must be added to the $casts property on your model.

You might for example always show an HTTP status code attribute on your model as an integer:

protected $casts = [
    'status_code' => 'integer',
];

In this way, even when the field status_code is stored as the string "500" in the database, it will always be cast to the integer 500 when you access it.

Laravel supports some default cast types, and since Laravel 7 it is also possible to create your own custom cast classes with their get and set methods.

This also gives the ability to cast compound values, i.e. value object casting, which we will look into right now.

How to use value object casting?

In the previous example, we only cast the value to a primitive type, i.e. a string.

With value object casting it is possible to cast multiple values from the database to a single object that you can add to your model. You can also transform this object to multiple values when it has to be stored.

Let's explore this, by creating a value object for a planet's (average) distance from earth. This distance is part of the Planet model, which will be stored in the planets table:

<?php

namespace App\VauleObjects;

use Assert\Assert;

final class DistanceFromEarth
{
    private int $measurement;
    private string $unit;

    public function __construct(int $measurement, string $unit)
    {
        Assert::that($measurement)->greaterThan(0);
        Assert::that($unit)->inArray(['KM', 'MI', 'AU', 'LY']);

        $this->measurement = $measurement;
        $this->unit = $unit;
    }

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

    public function showUnit(): string
    {
        return $this->unit;    
    }
}

When creating the DistanceFromEarth object, each value is validated in the constructor. We will use the beberlei/assert library for this, in order that an error will be thrown when validaiton fails.

The example of the value object in the Laravel documentation about custom casts contains public properties, probably in order to provide a brief and clear example. Since this is not recommended for value objects, the DistanceFromEarth object has two private properties, and two accompanying query methods.

We also have to define a custom cast class, called DistanceCast:

<?php

namespace App\Casts;

use App\ValueObjects\DistanceFromEarth;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;

class DistanceCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        return new DistanceFromEarth(
            $attributes['measurement'],
            $attributes['unit']
        );
    }

    public function set($model, $key, $value, $attributes)
    {
        if (!$value instanceof DistanceFromEarth) {
            throw new InvalidArgumentException(
                'The given value is not a DistanceFromEarth instance.'
            );
        }

        return [
            'measurement' => $value->showMeasurement(),
            'unit' => $value->showUnit(),
        ];
    }
}  

In this case, we must assume that the planets table has both a measurement column and a unit column.

Finally, we will add the DistanceCast class to our Planet model:

<?php

namespace App;

class Planet
{
    protected $casts = [
        'distance' => DistanceCast::class,
    ];
}

Now, when we want to create a new Planet model, we can set the distance like this:

use App\Planet;
use App\ValueObjects\DistanceFromEarth;

/** @test */
public function it_stores_a_distance_object()
{
    $planet = new Planet();

    $planet->name = 'Mars';
    $planet->distance = new DistanceFromEarth(
        225000000,
        'KM'
    );

    $planet->save();

    // true
    $this->assertDatabaseHas('planets', [
        'name' => 'Mars',
        'measurement' => 225000000,
        'unit' => 'KM',
    ]);
}

When reading form the Planet model, it is possible to return the DistanceFromEarth object, by accessing $planet->distance:

use App\Planet;
use App\ValueObjects\DistanceFromEarth;

/** @test */
public function it_returns_a_distance_object()
{
    $planet = Planet::create([
        'name' => 'Mars',
        'measurement' => 225000000,
        'unit' => 'KM',
    ]);

    // true
    $this->assertEquals(new DistanceFromEarth(225000000, 'KM'), $planet->distance);
}

When a value is incorrect, an exception will be thrown from the DistanceFromEarth object before the model is stored:

use App\Planet;
use App\ValueObjects\DistanceFromEarth;
use Assert\InvalidArgumentException;

/** @test */
public function it_validates_the_distance()
{
    // true
    $this->expectException(InvalidArgumentException::class);

    $planet = new Planet();

    $planet->name = 'Mars';
    $planet->distance = new DistanceFromEarth(
        225000000,
        'METERS'
    );
}

Using value object casting allows you to introduce (already existing) value objects to your eloquent models, gives the ability to validate certain values before saving and retrieving them, and makes it possible to cast the values to another type.

All examples can be found in the 7-x-features repository on GitHub, including unit tests.