#laravel#clean-code#php#oop

Custom Eloquent Casts & Value Objects in Laravel

"Clean up Laravel Eloquent models with custom Casts and domain-driven Value Objects."

By huud
0 views
~2 min read
Custom Eloquent Casts & Value Objects in Laravel

Custom Eloquent Casts & Value Objects in Laravel

Storing complex structures (such as multi-currency amounts, JSON address payloads, or geo-coordinates) directly on Eloquent models often results in bloated models filled with repetitive accessors and mutators.

With Custom Casts, raw database values can be cast directly to immutable Value Objects.


1. Creating the Value Object

Create an immutable Value Object, for example for monetary values:

php
namespace App\ValueObjects;

use InvalidArgumentException;

readonly class Money
{
    public function __construct(
        public int $amountInCents,
        public string $currency = 'USD'
    ) {
        if ($this->amountInCents < 0) {
            throw new InvalidArgumentException('Amount cannot be negative.');
        }
    }

    public function formatted(): string
    {
        return '$' . number_format($this->amountInCents / 100, 2);
    }
}

2. Implementing the Custom Cast

Implement Illuminate\Contracts\Database\Eloquent\CastsAttributes:

php
namespace App\Casts;

use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

class MoneyCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
    {
        if ($value === null) {
            return null;
        }

        return new Money(
            (int) $value,
            $attributes['currency'] ?? 'USD'
        );
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        if ($value === null) {
            return null;
        }

        if (! $value instanceof Money) {
            throw new InvalidArgumentException('The given value is not a Money instance.');
        }

        return $value->amountInCents;
    }
}

3. Registering the Cast on the Model

Use Laravel 11's method-based casts() definition:

php
namespace App\Models;

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

class Product extends Model
{
    protected function casts(): array
    {
        return [
            'price' => MoneyCast::class,
        ];
    }
}

4. Usage

php
// Persisting data
$product = new Product();
$product->name = 'Mechanical Keyboard';
$product->currency = 'USD';
$product->price = new Money(15000, 'USD'); // $150.00
$product->save();

// Retrieving data
$product = Product::find(1);
echo $product->price->formatted(); // "$150.00"

Key Benefits

  1. Type Safety: Enforces domain constraints rather than passing untyped primitives.
  2. Encapsulation: Domain calculations and formatting live inside the Value Object.
  3. Clean Models: Keeps Eloquent models concise and free of accessor clutter.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.