Eloquent Custom Casts: Value Objects &amp; Encryption | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com) - [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [About](https://www.msaied.com#about)

           [  Contact](https://www.msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com)
- [ProjectsCase studies](https://www.msaied.com/projects)
- [ArticlesEngineering notes](https://www.msaied.com/articles)
- [CertificatesCredentials](https://www.msaied.com/certificates)
- [AboutHow I work](https://www.msaied.com#about)
- [ContactGet in touch](https://www.msaied.com#contact)

  [Start a conversation](https://www.msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com)
2. /
3. [Articles](https://www.msaied.com/articles)
4. /
5. Eloquent Custom Casts: Encrypting, Compressing, and Composing Value Objects at the Model Layer

 Eloquent Custom Casts: Encrypting, Compressing, and Composing Value Objects at the Model Layer
===============================================================================================

 Go beyond built-in Eloquent casts. Learn to build custom CastsAttributes implementations that encrypt, compress, and hydrate rich value objects — keeping your models clean and your domain logic portable.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com#person) Published 19 Oct 2026 · Updated 19 Oct 2026 · 3 min read

ShareCopy linkCopied

 ![Eloquent Custom Casts: Encrypting, Compressing, and Composing Value Objects at the Model Layer](https://cdn.msaied.com/796/13df3306c87eeeccf69b380e7a07d8da.png) 

  On this page +1. [Why Custom Casts Deserve More Attention](#why-custom-casts-deserve-more-attention)
2. [The Contract](#the-contract)
3. [Parameterised Casts](#parameterised-casts)
4. [Combining Encryption and Compression](#combining-encryption-and-compression)
5. [Inbound-Only Casts](#inbound-only-casts)
6. [Composing Casts with withCastParameters](#composing-casts-with-codewithcastparameterscode)
7. [Takeaways](#takeaways)

 Why Custom Casts Deserve More Attention
---------------------------------------

Laravel ships with a solid set of built-in casts — `integer`, `encrypted`, `AsCollection`, `AsEnumCollection` — but the moment your domain needs a `Money`, `Temperature`, or `GeoPoint` value object, you're back to mutators and boilerplate. The `CastsAttributes` contract is the clean answer, and it composes better than you might expect.

---

The 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
    {
        $decoded = json_decode($value, true, flags: JSON_THROW_ON_ERROR);
        return new Money($decoded['amount'], Currency::from($decoded['currency']));
    }

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

```

Attach it on the model:

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

```

Now `$order->price` is always a `Money` object. No mutator, no accessor, no leaking raw integers into view logic.

---

Parameterised Casts
-------------------

Casts accept constructor arguments via the colon syntax. Use this to make a single cast reusable across different precision requirements:

```php
class RoundedDecimalCast implements CastsAttributes
{
    public function __construct(private readonly int $scale = 2) {}

    public function get(Model $model, string $key, mixed $value, array $attributes): string
    {
        return number_format((float) $value, $this->scale, '.', '');
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return bcscale($this->scale) ? bcdiv((string) $value, '1', $this->scale) : (string) $value;
    }
}

```

```php
'tax_rate' => RoundedDecimalCast::class . ':4',
'discount'  => RoundedDecimalCast::class . ':2',

```

---

Combining Encryption and Compression
------------------------------------

Storing large JSON blobs encrypted is expensive. Compress first, then encrypt:

```php
class CompressedEncryptedCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        if ($value === null) return null;
        $decrypted = decrypt($value);
        $decompressed = gzinflate(base64_decode($decrypted));
        return json_decode($decompressed, true, flags: JSON_THROW_ON_ERROR);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): ?string
    {
        if ($value === null) return null;
        $json = json_encode($value, JSON_THROW_ON_ERROR);
        $compressed = base64_encode(gzdeflate($json, 9));
        return encrypt($compressed);
    }
}

```

This pattern is useful for audit payloads, GDPR-sensitive metadata, or any column where you need both confidentiality and storage efficiency without a separate pipeline step.

---

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

Implement `CastsInboundAttributes` when you only need to transform on write — for example, hashing a token before storage:

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

class Sha256Cast implements CastsInboundAttributes
{
    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        return hash('sha256', $value);
    }
}

```

Reading returns the raw hash from the database, which is exactly what you want for comparison operations.

---

Composing Casts with `withCastParameters`
-----------------------------------------

For complex value objects that need injected services (e.g., a currency converter), avoid reaching into the container inside the cast. Instead, pass configuration through parameters and keep the cast stateless. If you genuinely need a service, register a custom cast class in the container and bind it via `Model::resolveRelationUsing` — but that's a code smell worth questioning first.

---

Takeaways
---------

- Implement `CastsAttributes` to hydrate domain value objects directly on Eloquent models.
- Use constructor parameters (colon syntax) to make a single cast class reusable with different configurations.
- Combine `gzdeflate` + `encrypt` in one cast to handle large sensitive blobs without a separate pipeline.
- `CastsInboundAttributes` is the right tool when the read path should return raw database values.
- Keep casts stateless; if you need a service, reconsider whether the cast is the right boundary.

- [laravel](https://www.msaied.com/articles?search=laravel)
- [eloquent](https://www.msaied.com/articles?search=eloquent)
- [casts](https://www.msaied.com/articles?search=casts)
- [value-objects](https://www.msaied.com/articles?search=value-objects)
- [domain-driven-design](https://www.msaied.com/articles?search=domain-driven-design)

 Frequently asked questions 
---------------------------

  Can a custom cast return null safely without throwing?Yes. Guard against null in both `get` and `set` by returning null early. Laravel passes null through to your cast when the column value is NULL, so explicit null checks are your responsibility.

   Does using a custom cast affect Eloquent dirty-checking?It can. Eloquent compares the serialised (set) representation for dirty detection. If your cast produces a non-deterministic string (e.g., unordered JSON keys), the model may appear dirty on every save. Use `JSON\_FORCE\_OBJECT` or sort keys before encoding to keep comparisons stable.

   How do I unit-test a custom cast in isolation?Instantiate the cast class directly and call `get` or `set` with a plain `new class extends Model {}` stub. You don't need a database connection to test cast logic, which makes these fast pure-unit tests.

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

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

[About](https://www.msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleEloquent Query Optimization: Defeating N+1 Problems at the Source](https://www.msaied.com/articles/eloquent-query-optimization-defeating-n1-problems-at-the-source)  

   On this page
-------------

1. [Why Custom Casts Deserve More Attention](#why-custom-casts-deserve-more-attention)
2. [The Contract](#the-contract)
3. [Parameterised Casts](#parameterised-casts)
4. [Combining Encryption and Compression](#combining-encryption-and-compression)
5. [Inbound-Only Casts](#inbound-only-casts)
6. [Composing Casts with withCastParameters](#composing-casts-with-codewithcastparameterscode)
7. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/795/9d9dd20c259ecc6356a91021a872695c.png)  · 3 min read### Eloquent Query Optimization: Defeating N+1 Problems at the Source

18 Oct 2026 ](https://www.msaied.com/articles/eloquent-query-optimization-defeating-n1-problems-at-the-source) [ ![](https://cdn.msaied.com/794/ceed213d17627e0cfd9535e4ae909775.png)  · 3 min read### Filament v4 Custom Table Columns: Building Reusable, Typed Column Components

18 Oct 2026 ](https://www.msaied.com/articles/filament-v4-custom-table-columns-building-reusable-typed-column-components) [ ![](https://cdn.msaied.com/793/9ec9d2cf138593ecc4292fa9fa49ee7a.png)  · 3 min read### Testing Filament Resources, Actions, and Form Assertions with Pest

18 Oct 2026 ](https://www.msaied.com/articles/testing-filament-resources-actions-and-form-assertions-with-pest-5) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/sitemap.xml)
