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

> Use a custom cast — that's exactly what casts are for. One CastsAttributes class works across every model; accessors mean per-model duplication. Integer cents in the DB, an immutable Money on the model.

- Asked: 2026-07-29
- Answered: 2026-08-04
- Asked by: Sıla
- Tags: laravel, eloquent, casts
- Source: https://www.muhammetsafak.com.tr/en/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`). Casts are designed precisely for value object ↔ column mapping; accessors/mutators are the older, clunkier tool for a single-column VO.

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. **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`.

3. **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.

4. **Keep Money immutable.** 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.

5. **Remember 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; expose a `cents()` on the VO and use it in queries.

6. **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. Non-constant parameters (like a default currency) can be passed to the cast via `castUsing`.

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];
}
```

**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`.
