Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Currency Casting Laravel Package

snoeren-development/laravel-currency-casting

Laravel Currency Casting provides an Eloquent cast that stores monetary values as integers and exposes them as currency-aware objects, so you can format, convert, and calculate prices cleanly across your models without scattering money-handling logic throughout your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin.

composer require snoeren-development/laravel-currency-casting

Confirm the resolved version is ^1.11 if you are on (or heading to) Laravel 13. Before you commit, read the require block in vendor/snoeren-development/laravel-currency-casting/composer.json — the v1.11.0 release is a framework-constraint bump, so the two things that actually matter are the laravel/framework range and the php floor. If your runtime image is on an older PHP minor than the new floor, you will find out at deploy time, not at composer require time.

Where to look first.

  1. src/ — find the cast class. There is essentially one public API surface here, and it is the cast. Note its exact FQCN; you will type it in $casts on every money-bearing model.
  2. config/ — if the package ships a config file, run php artisan vendor:publish --tag=... and read it. This is where the default currency code lives.
  3. tests/ — the package's own tests are the most accurate documentation of edge-case behavior (null handling, negative amounts, rounding).

The one thing to resolve before writing code. The package description says two different things: "cast currency attributes stored as integer to floats" and "converts stored values into Money instances." Those are different contracts. Open the cast class and check what get() returns. Everything below assumes it returns a value object with a format()/amount() style API — but if it returns a plain float, your formatting and arithmetic strategy changes completely. Settle this in five minutes rather than discovering it in a Blade template.

First use case. Take an existing integer column — say orders.total_cents — and cast it:

use SnoerenDevelopment\CurrencyCasting\Casts\...; // confirm FQCN from src/

protected $casts = [
    'total_cents' => CurrencyCast::class, // or ::class.':EUR' if per-attribute currency is supported
];

Then $order->total_cents gives you the formatted/typed value, while $order->getRawOriginal('total_cents') still gives you the untouched integer. That raw accessor is your escape hatch and you will use it constantly.


Implementation Patterns

Cast at the boundary, not everywhere. Treat the cast as a presenter for reads and a coercion for writes. Storage stays integer/bigint — do not migrate columns to decimal or float to "match" the cast. The whole value proposition is precise integer storage with ergonomic reads.

Per-model vs. per-attribute currency. Two viable shapes:

  • Single-currency model (most apps): set the currency once via config, cast the attribute plainly.
  • Multi-currency (marketplaces, multi-tenant billing): pass the code as a cast parameter where supported, or keep an adjacent currency_code column and format explicitly in the presenter layer. Do not rely on config for a multi-currency table — config is global, and the moment two rows have different currencies you have a bug that only shows up in reports.

Formatting belongs in the resource/presenter layer, not the model. Keep Eloquent returning the typed value and let JsonResource / Blade components decide the string:

// app/Http/Resources/OrderResource.php
public function toArray($request): array
{
    return [
        'total' => $this->total_cents->format(),      // "€1.234,56"
        'total_raw' => $this->getRawOriginal('total_cents'),
    ];
}

Emitting both the display string and the raw integer is the single highest-value habit with this package — clients get something to render, and integrations/analytics get something to sum.

Arithmetic. Do addition, subtraction, and summation on the underlying integers (or on the Money object's own add/subtract if it has them), then cast only for output. If the cast returns a Money object, never do $a->amount + $b->amount in float space and re-wrap; use the object's own operators so rounding rules live in one place. For aggregates, prefer the database:

Order::sum('total_cents'); // integer, exact

…and wrap the result for display, rather than ->get()->sum(fn ($o) => $o->total_cents->amount()), which hydrates the whole set and drifts.

Writes and input. Casts run on assignment, so $order->total_cents = 1999 and $order->total_cents = '19.99' may behave differently. Normalize at the FormRequest edge: accept a decimal string from the UI, convert it to integer minor units once in a prepareForValidation() or a dedicated MoneyInput cast, and let the model deal only in integers. This keeps the package's coercion from being load-bearing on user input.

Factories, seeders, and tests. Write factories in raw integers — that is what the column stores and it removes the cast from your fixture path. In assertions, compare raw:

$this->assertDatabaseHas('orders', ['total_cents' => 1999]);

If you assert on the casted attribute, assert against the Money object (or formatted string) explicitly so the test documents which contract you rely on.

Config and caching. Any config you publish is subject to php artisan config:cache. Change the default currency and forget to re-cache in production and you get stale formatting — the classic symptom is a deploy that "didn't take."

Blade. A one-line component wrapping the format call keeps templates readable and gives you a single place to swap in Number::currency() from Laravel's own helpers if you later want locale-aware output without the package's formatting.


Gotchas and Tips

Float precision is the trap, not the package. The README's "integer to floats" phrasing is the danger zone. 0.1 + 0.2 in float space is not 0.3, and a cast that hands you floats will round-trip through float when isDirty() compares against the original integer. Symptom: a model that is "dirty" on every save with no changes, or $0.01 discrepancies in totals. Verify with:

$order->total_cents = 1999;
$order->save();
$order->refresh();
dump($order->getRawOriginal('total_cents'), $order->isDirty());

If isDirty() is true right after refresh(), the cast's set() is not producing an integer-equal value. Work around it by always assigning integers, or by overriding set() in a subclass.

Where clauses do not go through casts. ->where('total_cents', '>', 19.99) will not do what you want. Query in raw minor units, always. Same for orderBy and any having clause.

JSON serialization. If the cast returns a Money object, toArray()/toJson() on the model may serialize the object (nested amount/currency keys) rather than a number. That will silently change your API contract. Pin it explicitly with $hidden plus a resource, or override serializeDate-style behavior via an accessor. Check $model->toArray() before and after adding the cast — that diff is your API breaking-change review.

Aggregates and sum/avg. avg() returns a float even on integer columns; do not feed it back into a currency attribute without rounding to minor units first.

Negative amounts and refunds. Test -1999. A cast that formats via absolute value, or that mis-handles the sign when parsing a decimal string, will produce a refund that displays as a charge. Cheap to test, expensive to miss.

Nullability. Decide what a null money attribute means (unknown vs. zero) and confirm the cast preserves null rather than coercing to 0. Coercing to zero destroys the distinction and is nearly impossible to unwind later.

Currency mismatch. Nothing in a cast layer stops you from adding a EUR Money to a USD Money. If the object supports it, it will either throw or silently add — both bad in different ways. Assert currency equality at the domain boundary, not in the view.

Extension points. Everything here is a cast, so the extension point is subclassing it: override get()/set() to add rounding policy, locale formatting, or null semantics. Because it is a single class, a local subclass is a much cheaper fork than vendoring the package.

The Laravel 13 constraint is additive — verify it. composer why-not laravel/framework 13.* and composer why-not snoeren-development/laravel-currency-casting 1.11 are the two commands that tell you whether this package is your blocker or someone else's. If v1.11.0 narrowed rather than widened support, you cannot take it on Laravel 12 and your package bump and framework bump collapse into one atomic change — a material difference in scheduling, and worth confirming in the composer.json diff rather than the release title.

Debugging order of operations. When a money attribute looks wrong, check in this order: (1) the raw column value in the DB, (2) getRawOriginal() on the model, (3) the casted value, (4) the formatted output. Ninety percent of "the cast is broken" reports stop at step 1.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast