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 Prices Laravel Package

whitecube/laravel-prices

Store and work with money in Laravel without floating-point errors. whitecube/laravel-prices wraps integer minor-unit amounts in a Price object with currency support, arithmetic, formatting helpers, and Eloquent casts, so prices stay precise and readable throughout your application.

View on GitHub
Deep Wiki
Context7

Getting Started

whitecube/laravel-prices is a thin, opinionated wrapper around moneyphp/money that makes monetary values first-class citizens in Eloquent models. Instead of juggling raw integers and NumberFormatter calls all over your views, you get a Price object you can cast attributes to, do arithmetic on, and format for display.

Requirements (as of v2.0.0): PHP 8.3+ and Laravel 11+. This release dropped PHP 8.2 and Laravel 10, so verify your CI matrix and composer.json constraints before bumping.

First steps:

  1. Install and let Composer resolve the version:
    composer require whitecube/laravel-prices
    
  2. Check whether a config file exists and publish it if so — it typically holds a default currency used when a cast doesn't specify one:
    php artisan vendor:publish --tag=prices-config
    
  3. Read src/Price.php and src/Casts/ first. The package is small; the cast classes tell you exactly what storage shape each one expects, which is the single most important thing to get right on day one.

First use case: add a price column and cast it.

// migration — store minor units (cents) as an integer
$table->integer('price')->default(0);

// model
use Whitecube\LaravelPrices\Casts\PriceCast;

protected $casts = [
    'price' => PriceCast::class.':EUR',
];
$product->price = Price::EUR(1999); // €19.99
$product->save();

$product->price->format(); // "€19.99" (locale-aware)

The mental model to adopt immediately: the database stores integers, the application passes Price objects around, and only the view layer formats. Every other decision follows from that.

Implementation Patterns

Casting is the primary integration point. Pick the cast that matches your schema and stick to it consistently:

  • A plain price cast when currency is implicit (single-currency app, currency from config or fixed in the cast string).
  • A cast variant that persists the currency code alongside the amount when you need multi-currency rows.
  • A discount/VAT-aware cast when list price and effective price differ and you want that delta modelled rather than recomputed in controllers.
  • A currency cast for the column holding the ISO code itself, so $model->currency is an enum-ish value rather than a loose string.

Keep arithmetic inside the package. Never $price->amount() / 100 in a controller. Compose instead:

$subtotal = $lineItems->reduce(
    fn (?Price $carry, LineItem $item) => $carry?->add($item->total) ?? $item->total
);

$taxed = $subtotal->add($subtotal->multiply(0.21));

Price values are immutable, so add()/subtract()/multiply() return new instances — this makes them safe to pass into Blade components and API resources without defensive cloning.

Parse at the boundary, format at the edge. User input arrives as a string; convert it once in a FormRequest and let Price objects flow through the rest of the stack:

protected function prepareForValidation(): void
{
    $this->merge(['price' => Price::parse($this->input('price'))]);
}

Format per-render, not per-store. Call ->format() inside Blade / API resources only. If you need multiple locales, pass the locale at format time rather than mutating the underlying value.

Discounts and VAT are a workflow, not a helper. For catalogues, store the base price and the discount rule separately, then expose an accessor that composes them:

protected function effectivePrice(): Attribute
{
    return Attribute::get(fn () => $this->price->subtract($this->discount));
}

Test against the object, not the integer. Because the cast round-trips through the database, an assertion like $this->assertEquals(Price::EUR(1999), $product->fresh()->price) catches scale/rounding mistakes that assertDatabaseHas(['price' => 1999]) will happily miss.

API resources. Either let the cast's JSON representation through (check what the cast yields — it is usually a normalised array or string), or be explicit:

return [
    'amount' => $this->price->format(),
    'currency' => $this->price->currency()->getCode(),
];

Being explicit is strongly recommended for public APIs; the serialised shape is the package's concern, not your contract.

Gotchas and Tips

Minor units, always. Price::EUR(1999) is €19.99, not €1999. Half the confusion people report with money packages is scale: integers in the DB are in the currency's smallest unit, and JPY/KWD do not have two decimal places. The package inherits this from moneyphp/money, including its exceptions when scales mismatch.

Never use floats as an intermediate. Price::fromFloat(19.99) style conversions invite binary rounding errors. Parse strings ("19.99") or construct from integer minor units. If you must accept a float from a legacy API, round explicitly before constructing.

Currency mismatch throws. Adding EUR to USD raises an exception rather than silently converting. This is good — it catches bugs early — but it means a multi-currency order total needs a conversion strategy (your own, usually) before you call add(). Decide and document where that conversion lives.

Cast string syntax is easy to fumble. PriceCast::class.':EUR' requires the colon and no spaces. A malformed cast parameter typically surfaces as an unexpected default currency rather than a hard error, so add a test that asserts the currency on a freshly retrieved model.

Nullability. A nullable price column will hand you null from the cast, so every consumer needs ?->. Either make the column non-nullable with a 0 default or write an accessor that returns a zero Price — mixing the two behaviours across models is a common source of Call to a member function format() on null.

Upgrading to v2.0.0 is a dependency bump, not an API migration. The changelog lists only platform support changes (adds Laravel 13 / PHP 8.5, removes Laravel 10 / PHP 8.2), with no mentioned API changes. In practice:

composer why-not whitecube/laravel-prices ^2.0
composer update whitecube/laravel-prices --with-dependencies
php artisan test

If why-not blames a package pinning laravel/framework ^10, that dependency — not this one — is your blocker.

CI matrix. Bump php-version to 8.3/8.4/8.5 and laravel to 11.*/12.*/13.*. Dropping PHP 8.2 is the change most likely to break an otherwise-green pipeline, since it fails at install time rather than test time.

Formatting is locale-sensitive. ->format() respects the app locale and can change output between environments if your locale is set from the request. Snapshot tests asserting exact strings should pin the locale explicitly.

Extension points are limited by design. The package is a convenience layer; when you need exotic behaviour (custom rounding strategies, currency conversion, tax rules), drop down to the underlying Money object rather than fighting the wrapper, and keep that escape hatch in one place — a dedicated service class — so a future major version can't ripple through your codebase.

Watch the upstream repo, not just the changelog. Because the package tracks the Laravel release cycle, a new major usually lands within weeks of a Laravel major. Plan the bump alongside your framework upgrade rather than as an afterthought.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings