Custom Eloquent Casts for Domain Logic in Laravel | 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: Encapsulating Domain Logic Inside Model Attributes        On this page       1. [  Why Custom Casts Beat Accessor/Mutator Pairs ](#why-custom-casts-beat-accessormutator-pairs)
2. [  The Interface Contract ](#the-interface-contract)
3. [  Registering the Cast ](#registering-the-cast)
4. [  Parameterised Casts ](#parameterised-casts)
5. [  Inbound-Only Casts ](#inbound-only-casts)
6. [  Testing Your Cast in Isolation ](#testing-your-cast-in-isolation)
7. [  Handling Null Gracefully ](#handling-null-gracefully)
8. [  When Not to Use a Cast ](#when-not-to-use-a-cast)
9. [  Takeaways ](#takeaways)

  ![Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes](https://cdn.msaied.com/238/8e843e57a34f81f853eedefae629c09b.png)

  #laravel   #eloquent   #domain-driven-design   #php  

 Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes 
===========================================================================

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

       Table of contents

  9 sections  

1. [  01   Why Custom Casts Beat Accessor/Mutator Pairs  ](#why-custom-casts-beat-accessormutator-pairs)
2. [  02   The Interface Contract  ](#the-interface-contract)
3. [  03   Registering the Cast  ](#registering-the-cast)
4. [  04   Parameterised Casts  ](#parameterised-casts)
5. [  05   Inbound-Only Casts  ](#inbound-only-casts)
6. [  06   Testing Your Cast in Isolation  ](#testing-your-cast-in-isolation)
7. [  07   Handling Null Gracefully  ](#handling-null-gracefully)
8. [  08   When Not to Use a Cast  ](#when-not-to-use-a-cast)
9. [  09   Takeaways  ](#takeaways)

       Why Custom Casts Beat Accessor/Mutator Pairs
--------------------------------------------

Before Laravel 8 introduced `CastsAttributes`, the standard approach was a `getXAttribute` / `setXAttribute` pair. That works, but it scatters transformation logic across two methods, makes the intent implicit, and is impossible to reuse across models. A custom cast is a self-contained class: one place to read, one place to write, and trivially injectable into any model.

### The Interface Contract

```php
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

/**
 * @implements CastsAttributes
 */
class MoneyCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): Money
    {
        if (is_null($value)) {
            return Money::zero();
        }

        $decoded = json_decode($value, true, flags: JSON_THROW_ON_ERROR);

        return new Money(
            amount: $decoded['amount'],
            currency: Currency::from($decoded['currency'])
        );
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        if ($value instanceof Money) {
            return json_encode([
                'amount'   => $value->amount,
                'currency' => $value->currency->value,
            ], JSON_THROW_ON_ERROR);
        }

        throw new \InvalidArgumentException('Value must be a Money instance.');
    }
}

```

The generic annotation on the `@implements` docblock is picked up by PHPStan and Psalm, giving you typed attribute access without any extra stubs.

### Registering the Cast

```php
class Order extends Model
{
    protected $casts = [
        'total' => MoneyCast::class,
    ];
}

```

Now `$order->total` is always a `Money` object. No defensive `instanceof` checks in your service layer.

### Parameterised Casts

Sometimes you need the cast to behave differently per attribute — for example, rounding precision. Pass arguments via the colon syntax:

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

```

Receive them in the constructor:

```php
class MoneyCast implements CastsAttributes
{
    public function __construct(protected int $precision = 2) {}

    // get / set use $this->precision
}

```

### Inbound-Only Casts

If you only need to transform on write (e.g., hashing a PIN), implement `CastsInboundAttributes` instead. It has only a `set` method, which makes the intent explicit and prevents accidental reads of the raw value.

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

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

```

### Testing Your Cast in Isolation

Because the 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 Order();

    $json = $cast->set($model, 'total', new Money(1999, Currency::GBP), []);
    $back = $cast->get($model, 'total', $json, []);

    expect($back->amount)->toBe(1999)
        ->and($back->currency)->toBe(Currency::GBP);
});

```

No factories, no migrations, no HTTP — just fast, focused assertions.

### Handling Null Gracefully

Eloquent passes `null` to `get` when the column is `NULL`. Always decide explicitly: return a null object, throw, or return `null` and mark the property nullable in your type hint. Returning a null object (like `Money::zero()`) is usually the safest choice for arithmetic-heavy domains.

### When Not to Use a Cast

Casts are evaluated on every attribute access. If your value object is expensive to construct (e.g., it calls a service or parses a large blob), consider lazy construction or caching the result in a model property instead.

Takeaways
---------

- `CastsAttributes` gives you a single, reusable class for bidirectional transformation — no more scattered accessor/mutator pairs.
- Parameterised casts via the colon syntax keep one class flexible across multiple attributes.
- Use `CastsInboundAttributes` for write-only transformations like hashing.
- Casts are plain PHP classes: unit-test them without a database for fast feedback loops.
- Always handle `null` explicitly to avoid silent type errors downstream.
- Avoid expensive operations inside `get`; casts run on every property read.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcustom-eloquent-casts-encapsulating-domain-logic-inside-model-attributes&text=Custom+Eloquent+Casts%3A+Encapsulating+Domain+Logic+Inside+Model+Attributes) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcustom-eloquent-casts-encapsulating-domain-logic-inside-model-attributes) 

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

  3 questions  

     Q01  Can a custom cast return null instead of a value object?        Yes. If the column is nullable and a null object pattern does not fit your domain, simply return null from `get` and annotate the property type as nullable. Just be consistent so callers always know what to expect. 

      Q02  Do custom casts work with Eloquent's `isDirty` and `getChanges` methods?        Yes, but Eloquent compares the raw stored value, not the cast object. If you need dirty-checking on the value object level, override `castForComparison` or compare objects yourself in a model observer. 

      Q03  Is there a performance cost to using custom casts on frequently accessed attributes?        The cast's `get` method runs on every attribute access unless you cache the result. For lightweight value objects the overhead is negligible, but for expensive construction consider storing the result in a model property after the first access. 

  Continue reading

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

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

 [ ![PostgreSQL CTEs, Recursive Queries, and Lateral Joins in Laravel](https://cdn.msaied.com/241/32858f9c67eae0649999c32a6d31818f.png) laravel postgresql query-builder 

### PostgreSQL CTEs, Recursive Queries, and Lateral Joins in Laravel

Go beyond basic Eloquent with raw PostgreSQL power: composable CTEs, recursive tree traversal, and LATERAL joi...

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

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-ctes-recursive-queries-and-lateral-joins-in-laravel) [ ![PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://cdn.msaied.com/239/f588e7cbf8e6d3317a581ce0fa27140d.png) laravel postgresql eloquent 

### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

Window functions let you compute rankings, running totals, and gaps directly in SQL without pulling rows into...

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

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection) [ !['The Story of PHP' Documentary Teaser Is Out — Watch It Now](https://cdn.msaied.com/237/78daf2c90e319b7a740ec4d48d5280c6.png) PHP Laravel Documentary 

### 'The Story of PHP' Documentary Teaser Is Out — Watch It Now

CultRepo has released a teaser for 'The Story of PHP', an upcoming documentary sponsored by JetBrains. It feat...

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

 18 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/the-story-of-php-documentary-teaser-is-out-watch-it-now) 

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