# Should I bind a Money value object to Eloquent with a custom cast or with accessors/mutators?

> Integer cents in the DB, an immutable `Money` on the model: put the mapping in one `CastsAttributes` class and keep currency as a second column there.

- Asked: 2026-07-29
- Answered: 2026-08-04
- Asked by: Sıla
- Tags: laravel, eloquent, php
- Source: https://muhammetsafak.com/just-ask/should-i-bind-a-money-value-object-to-eloquent-with-a/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** I store amounts as integer cents in the database (like `price_cents`). I want a `Money` value object on my models, but I don't want the cents representation leaking everywhere — I don't want to keep doing `/100` in Blade, in the service layer, in tests.

In Laravel, is it more correct to bind this with a custom cast (`CastsAttributes`) or with classic accessors/mutators? I might later add a `currency` column next to the amount; how does that choice affect that case?


Short answer: use a custom cast (a class implementing `CastsAttributes`).

## Short answer

Casts are designed precisely for value object ↔ column mapping; accessors/mutators are the older, clunkier tool for a single-column VO. I wrote about how I design a value object like `Money` in modern PHP in [the value objects post](/blog/designing-value-objects-in-modern-php/); the question here is which hook you bind that object to Eloquent with.

## Why

1. **A custom cast is purpose-built for this.** You write one cast class with `get()`/`set()` and reuse it across every model and every column. Accessors are per-model, per-attribute methods; you'd rewrite the same Money logic on every model.

2. **The cast handles the multi-column case cleanly.** If amount and currency are two columns, the cast can read and write both via the `$attributes` array in `get()`/`set()`. `set()` can return an array like `['amount_cents' => ..., 'currency' => ...]` instead of a single attribute. Doing that with accessors/mutators — mapping two columns into one object and writing back to two — gets ugly. If you're adding currency later, that alone makes the cast the choice.

3. **The cast doesn't apply to the query builder.** The cast only runs on attribute get/set on the model; a query like `->where('price', $money)` doesn't go through it. In filters you still pass raw cents (`->where('price', $money->cents())`). This is where people trip most often, and it holds regardless of which option you pick.

## What to do

1. **Keep storage as integer cents.** In `set()` return the int cents to store; in `get()` build `Money::fromCents($value)`. Because the model always hands you a `Money`, the cents representation never leaks out and the rest of the codebase never sees `/100`. The single-column core looks like this:

   ```php
   /** @implements CastsAttributes<Money, Money> */
   final class MoneyCast implements CastsAttributes
   {
       public function get($model, string $key, $value, array $attributes): ?Money
       {
           return $value === null ? null : Money::fromCents((int) $value);
       }

       public function set($model, string $key, $value, array $attributes): ?int
       {
           return $value?->cents();
       }
   }

   // on the model:
   protected function casts(): array
   {
       return ['price' => MoneyCast::class];
   }
   ```

2. **Keep Money immutable and expose a `cents()` on it.** Make the VO `readonly`; keep money arithmetic (avoiding floats) and formatting there — not in Blade. Equality (`equals`) and currency-mismatch checks are the VO's job, not the cast's. Expose `cents()` for use in queries.

3. **Don't forget the edge cases.** Handle `null` in `get()`/`set()` for a nullable column, and keep the cast cheap since it runs on every hydration. Parameters (like a default currency) can be passed to the cast with the `:` cast-parameter syntax; they reach the cast's constructor.

**Bottom line:** personally I'd set up the trio of a custom cast + integer cents in the DB + an immutable `Money` VO, and manage currency as a second column from within that same cast. I'd keep accessors/mutators only for a trivial, model-specific computed field that doesn't even touch the VO. That way the cents representation stays behind the model, and every other layer only ever sees `Money`.

## Related Reading

- [Designing Value Objects in Modern PHP](/blog/designing-value-objects-in-modern-php/) — Blog
- [For comments across many models should I use a polymorphic relation or separate tables, given indexing and foreign keys?](https://muhammetsafak.com/just-ask/comments-polymorphic-relation-separate-tables-indexing-foreign-keys/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://muhammetsafak.com/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
- [Should I enable Model::preventLazyLoading only locally or in production too?](https://muhammetsafak.com/just-ask/should-i-enable-model-preventlazyloading-only-locally-or-in-production-too/) — Just Ask
