Advanced Eloquent Custom Casts &amp; Value Objects | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Advanced Eloquent Casts: Custom Cast Classes, Value Objects, and Inbound-Only Transforms        On this page       1. [  Why Built-in Casts Are Not Enough ](#why-built-in-casts-are-not-enough)
2. [  Anatomy of a Custom Cast ](#anatomy-of-a-custom-cast)
3. [  Value Objects as First-Class Citizens ](#value-objects-as-first-class-citizens)
4. [  Inbound-Only Casts ](#inbound-only-casts)
5. [  Casting to Multiple Columns ](#casting-to-multiple-columns)
6. [  Testing the Cast in Isolation ](#testing-the-cast-in-isolation)
7. [  Key Takeaways ](#key-takeaways)

  ![Advanced Eloquent Casts: Custom Cast Classes, Value Objects, and Inbound-Only Transforms](https://cdn.msaied.com/307/d9832b90141b009f63e8e55ea856cb3a.png)

  #laravel   #eloquent   #value-objects   #domain-driven-design   #testing  

 Advanced Eloquent Casts: Custom Cast Classes, Value Objects, and Inbound-Only Transforms 
==========================================================================================

     27 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why Built-in Casts Are Not Enough  ](#why-built-in-casts-are-not-enough)
2. [  02   Anatomy of a Custom Cast  ](#anatomy-of-a-custom-cast)
3. [  03   Value Objects as First-Class Citizens  ](#value-objects-as-first-class-citizens)
4. [  04   Inbound-Only Casts  ](#inbound-only-casts)
5. [  05   Casting to Multiple Columns  ](#casting-to-multiple-columns)
6. [  06   Testing the Cast in Isolation  ](#testing-the-cast-in-isolation)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why Built-in Casts Are Not Enough
---------------------------------

Laravel ships with a solid set of primitive casts — `integer`, `boolean`, `array`, `encrypted`, `AsCollection`, and friends. They cover 80% of everyday needs. The remaining 20% is where models quietly accumulate logic they should never own: formatting phone numbers, normalising currency, converting units, enforcing invariants.

Custom cast classes move that responsibility to a dedicated type, tested in isolation, reusable across models.

---

Anatomy of a Custom Cast
------------------------

A cast class implements `CastsAttributes`. The contract is simple:

```php
namespace App\Casts;

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

class MoneyCast implements CastsAttributes
{
    public function __construct(
        private readonly string $currency = 'GBP'
    ) {}

    /** @param int $value raw pence stored in DB */
    public function get(Model $model, string $key, mixed $value, array $attributes): Money
    {
        return Money::fromMinorUnits((int) $value, $this->currency);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): int
    {
        if ($value instanceof Money) {
            return $value->minorUnits();
        }

        return (int) $value;
    }
}

```

Declare it on the model using the constructor-argument syntax introduced in Laravel 9:

```php
protected function casts(): array
{
    return [
        'price' => MoneyCast::class . ':USD',
        'tax'   => MoneyCast::class,          // defaults to GBP
    ];
}

```

The model now returns a `Money` value object from `$order->price`, and accepts either a `Money` instance or a raw integer on assignment.

---

Value Objects as First-Class Citizens
-------------------------------------

A value object should be immutable and self-validating:

```php
final class Money
{
    private function __construct(
        private readonly int    $minorUnits,
        private readonly string $currency,
    ) {
        if ($minorUnits < 0) {
            throw new \DomainException('Money cannot be negative.');
        }
    }

    public static function fromMinorUnits(int $units, string $currency): self
    {
        return new self($units, strtoupper($currency));
    }

    public function minorUnits(): int  { return $this->minorUnits; }
    public function currency(): string { return $this->currency; }

    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \DomainException('Currency mismatch.');
        }
        return new self($this->minorUnits + $other->minorUnits, $this->currency);
    }

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

```

The invariant (`>= 0`) is enforced at construction time — not scattered across service classes.

---

Inbound-Only Casts
------------------

Sometimes you only need to transform data *on the way in* — hashing a token, normalising a slug, uppercasing a country code. Implement `CastsInboundAttributes` instead:

```php
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;

class NormaliseCountryCode implements CastsInboundAttributes
{
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return strtoupper(trim((string) $value));
    }
}

```

The `get` side is intentionally absent — the raw database value is returned as-is. This is perfect for write-time normalisation without the overhead of a full bidirectional cast.

---

Casting to Multiple Columns
---------------------------

A single value object can map to *multiple* database columns by returning an array from `set`:

```php
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
    return [
        'amount'   => $value->minorUnits(),
        'currency' => $value->currency(),
    ];
}

public function get(Model $model, string $key, mixed $value, array $attributes): Money
{
    return Money::fromMinorUnits(
        (int) $attributes['amount'],
        $attributes['currency'],
    );
}

```

Declare the virtual key on the model:

```php
'price' => MoneyCast::class,

```

Eloquent will merge the returned array into the dirty attributes automatically.

---

Testing the Cast in Isolation
-----------------------------

Because the cast is a plain PHP class, you can test it without booting the framework:

```php
it('converts minor units to a Money value object', function () {
    $cast  = new MoneyCast('EUR');
    $model = new class extends Model {};

    $money = $cast->get($model, 'price', 1999, []);

    expect($money)->toBeInstanceOf(Money::class)
        ->and($money->minorUnits())->toBe(1999)
        ->and($money->currency())->toBe('EUR');
});

it('rejects negative minor units', function () {
    expect(fn () => Money::fromMinorUnits(-1, 'EUR'))
        ->toThrow(\DomainException::class);
});

```

No database, no HTTP — fast, deterministic, and meaningful.

---

Key Takeaways
-------------

- Use `CastsAttributes` for bidirectional transforms; use `CastsInboundAttributes` when you only need write-time normalisation.
- Pass constructor arguments via the `ClassName:arg1,arg2` syntax to make casts reusable across currencies, locales, or units.
- Return an array from `set` to map a single virtual attribute to multiple physical columns.
- Keep value objects immutable and self-validating — the cast is just the bridge, not the domain logic.
- Test cast classes as plain PHP; no `RefreshDatabase` needed.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fadvanced-eloquent-casts-custom-cast-classes-value-objects-and-inbound-only-transforms&text=Advanced+Eloquent+Casts%3A+Custom+Cast+Classes%2C+Value+Objects%2C+and+Inbound-Only+Transforms) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fadvanced-eloquent-casts-custom-cast-classes-value-objects-and-inbound-only-transforms) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can a custom cast class be shared across multiple models?        Yes. A cast class is a plain PHP class with no model-specific coupling. Declare it in the `casts()` method of any model that needs it, optionally passing constructor arguments to vary behaviour per model. 

      Q02  What happens if the database column is NULL and my cast tries to construct a value object?        The `$value` parameter will be `null`. Guard against it explicitly in `get()` — either return `null` (and type-hint the return as `?Money`) or return a sensible default such as `Money::zero($currency)`. 

      Q03  Does returning an array from `set()` work with mass assignment and `fill()`?        Yes. Eloquent merges the returned array into the model's attributes before persisting. Ensure the physical column names (`amount`, `currency`) are included in `$fillable` or that `$guarded` is empty, otherwise the merge is silently dropped. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks](https://cdn.msaied.com/306/bfb43236ced4112cc9dab99a3eee82d2.png) filament laravel php 

### Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks

Go beyond built-in components: build a reusable Filament custom field plugin, a typed custom column, and wire...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/advanced-filament-custom-field-plugins-custom-columns-and-render-hooks) [ ![Livewire v3 Performance: Optimistic UI, Wire:model.live Debouncing, and Dirty State](https://cdn.msaied.com/305/98e4a28fdff5a8448e19534c07bb391d.png) livewire laravel performance 

### Livewire v3 Performance: Optimistic UI, Wire:model.live Debouncing, and Dirty State

Practical techniques for squeezing real performance out of Livewire v3: controlling when the server round-trip...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/livewire-v3-performance-optimistic-ui-wiremodellive-debouncing-and-dirty-state) [ ![Testing Filament Resources, Actions, and Form Assertions with Pest](https://cdn.msaied.com/304/2d16aec8179bbaae9647506470a85e40.png) filament pest testing 

### Testing Filament Resources, Actions, and Form Assertions with Pest

A practical guide to writing reliable Pest tests for Filament v3/v4 resources, covering table actions, form su...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/testing-filament-resources-actions-and-form-assertions-with-pest-1) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
