Custom Eloquent Casts: Value Objects &amp; Composites | 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)    Custom Eloquent Casts: Value Objects, Enums, and Encrypted Composites        On this page       1. [  Why Custom Casts Beat Accessors and Mutators ](#why-custom-casts-beat-accessors-and-mutators)
2. [  1. Value-Object Cast ](#1-value-object-cast)
3. [  2. Composite-Column Cast ](#2-composite-column-cast)
4. [  3. Encrypted JSON Cast with Constructor Arguments ](#3-encrypted-json-cast-with-constructor-arguments)
5. [  Testing Casts in Isolation ](#testing-casts-in-isolation)
6. [  Takeaways ](#takeaways)

  ![Custom Eloquent Casts: Value Objects, Enums, and Encrypted Composites](https://cdn.msaied.com/659/44f701e1dc43e64d0b7ecc984d0b34bc.png)

  #laravel   #eloquent   #ddd   #php  

 Custom Eloquent Casts: Value Objects, Enums, and Encrypted Composites 
=======================================================================

     12 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   Why Custom Casts Beat Accessors and Mutators  ](#why-custom-casts-beat-accessors-and-mutators)
2. [  02   1. Value-Object Cast  ](#1-value-object-cast)
3. [  03   2. Composite-Column Cast  ](#2-composite-column-cast)
4. [  04   3. Encrypted JSON Cast with Constructor Arguments  ](#3-encrypted-json-cast-with-constructor-arguments)
5. [  05   Testing Casts in Isolation  ](#testing-casts-in-isolation)
6. [  06   Takeaways  ](#takeaways)

 Why Custom Casts Beat Accessors and Mutators
--------------------------------------------

Getters and setters scattered across a model are hard to test in isolation and impossible to reuse across models. Laravel's `CastsAttributes` contract gives you a first-class way to encapsulate that logic into a dedicated class, keep your models thin, and make the transformation testable on its own.

This article covers three practical cast patterns: a value-object cast, a composite-column cast, and an encrypted-JSON cast.

---

1. Value-Object Cast
--------------------

Suppose you have a `Money` value object that wraps an integer amount and a currency string.

```php
// app/Values/Money.php
final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

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

```

The cast serialises to a single JSON column:

```php
// app/Casts/MoneyCast.php
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class MoneyCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): Money
    {
        $data = json_decode($value, true);
        return new Money($data['amount'], $data['currency']);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        if (! $value instanceof Money) {
            throw new \InvalidArgumentException('Expected Money instance.');
        }
        return json_encode(['amount' => $value->amount, 'currency' => $value->currency]);
    }
}

```

Usage on the model:

```php
protected $casts = [
    'price' => MoneyCast::class,
];

```

---

2. Composite-Column Cast
------------------------

Sometimes a logical concept spans multiple physical columns — for example, a `DateRange` stored as `starts_at` and `ends_at`. Implement `CastsAttributes` and return multiple keys from `set()`:

```php
class DateRangeCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): DateRange
    {
        return new DateRange(
            Carbon::parse($attributes['starts_at']),
            Carbon::parse($attributes['ends_at']),
        );
    }

    /** @return array */
    public function set(Model $model, string $key, mixed $value, array $attributes): array
    {
        return [
            'starts_at' => $value->start->toDateTimeString(),
            'ends_at'   => $value->end->toDateTimeString(),
        ];
    }
}

```

When `set()` returns an array, Eloquent merges those keys directly into the attributes bag — no extra columns needed in `$casts`.

> **Gotcha:** the `$key` passed to `get()` is whatever key you registered in `$casts`. For composite casts, that key is virtual; the real columns live in `$attributes`. Read from `$attributes`, not `$value`.

---

3. Encrypted JSON Cast with Constructor Arguments
-------------------------------------------------

Casts accept constructor arguments via the colon syntax. Here's an encrypted cast that accepts a cipher name:

```php
class EncryptedJson implements CastsAttributes
{
    public function __construct(private string $cipher = 'AES-256-CBC') {}

    public function get(Model $model, string $key, mixed $value, array $attributes): array
    {
        return json_decode(decrypt($value), true);
    }

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

```

Register it with an argument:

```php
protected $casts = [
    'metadata' => EncryptedJson::class . ':AES-128-CBC',
];

```

Laravel resolves the constructor automatically, passing the string after the colon as the first argument.

---

Testing Casts in Isolation
--------------------------

Because a cast is a plain PHP class, you can unit-test it without a database:

```php
it('round-trips a Money value object', function () {
    $cast  = new MoneyCast();
    $model = new class extends \Illuminate\Database\Eloquent\Model {};

    $json = $cast->set($model, 'price', new Money(1999, 'USD'), []);
    $back = $cast->get($model, 'price', $json, []);

    expect($back->amount)->toBe(1999)
        ->and($back->currency)->toBe('USD');
});

```

No factories, no migrations, no HTTP overhead.

---

Takeaways
---------

- `CastsAttributes` replaces accessor/mutator pairs with a reusable, testable class.
- Return an `array` from `set()` to hydrate multiple physical columns from one logical cast.
- Pass constructor arguments with the `ClassName:arg` colon syntax.
- Casts are plain PHP — unit-test them without touching the database.
- Combine with `readonly` value objects for immutable domain primitives that are impossible to corrupt.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcustom-eloquent-casts-value-objects-enums-and-encrypted-composites&text=Custom+Eloquent+Casts%3A+Value+Objects%2C+Enums%2C+and+Encrypted+Composites) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcustom-eloquent-casts-value-objects-enums-and-encrypted-composites) 

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

  3 questions  

     Q01  Can a custom cast hydrate columns that are not the key registered in $casts?        Yes. When set() returns an associative array, Eloquent merges every key in that array into the model's attributes, regardless of which key was registered in $casts. This is how composite casts spanning multiple columns work. 

      Q02  How do I pass runtime arguments to a custom cast?        Use the colon syntax in $casts: 'column' =&gt; MyCast::class . ':arg1,arg2'. Laravel splits on the colon and passes the comma-separated values as positional constructor arguments to the cast class. 

      Q03  Should I implement CastsInboundAttributes instead of CastsAttributes?        Use CastsInboundAttributes when you only need to transform data on the way into the database (e.g., hashing a password) and want the raw stored value returned on get. CastsAttributes is the right choice when both directions need transformation. 

  Continue reading

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

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

 [ ![Laravel Macro-Free Extensibility: Custom Query Builder Classes and Fluent Scopes](https://cdn.msaied.com/658/b45c3cc06b92a332e526bed9bb1f826d.png) laravel eloquent architecture 

### Laravel Macro-Free Extensibility: Custom Query Builder Classes and Fluent Scopes

Skip global macros and reach for typed, testable custom query builder classes in Laravel. Learn how to bind a...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 11 Sep 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-macro-free-extensibility-custom-query-builder-classes-and-fluent-scopes) [ ![Testing Filament Resources, Actions, and Form Assertions with Pest](https://cdn.msaied.com/655/efd33245cffa553c1dffba29721e0139.png) filament pest testing 

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

A practical guide to writing reliable Pest tests for Filament v3 resources — covering table actions, form subm...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 11 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/testing-filament-resources-actions-and-form-assertions-with-pest-4) [ ![PayZephyr: One Payment API for Stripe, Paystack, and PayPal in Laravel](https://cdn.msaied.com/657/2cc520b20de249b38bc52120605ff447.png) Laravel Payments Stripe 

### PayZephyr: One Payment API for Stripe, Paystack, and PayPal in Laravel

PayZephyr is a Laravel package that wraps eight payment providers—Stripe, Paystack, PayPal, Flutterwave, and m...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 11 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/payzephyr-one-payment-api-for-stripe-paystack-and-paypal-in-laravel) 

   [  ![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)
