aichadigital / lara100
Laravel cast for handling decimal values as base-100 integers (cents/centesimals)
Requires
- php: ^8.3|^8.4
- brick/math: ^0.14.2
- illuminate/contracts: ^12.0||^13.0
- illuminate/database: ^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.25
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.6||^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-mutate: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-06-25 06:30:10 UTC
README
A Laravel package providing an immutable, scale-configurable exact decimal value object (FixedDecimal) and a matching Eloquent cast (FixedDecimalCast). Values are stored as plain integers (unscaled) in the database; the cast and value object handle all precision arithmetic using brick/math under the hood.
Why Lara100?
Floating-point arithmetic is not safe for monetary or fiscal values:
0.1 + 0.2 === 0.3; // false — result is 0.30000000000000004
FixedDecimal eliminates this class of error entirely:
use AichaDigital\Lara100\ValueObjects\FixedDecimal; $a = FixedDecimal::ofDecimalString('0.1'); $b = FixedDecimal::ofDecimalString('0.2'); $c = $a->plus($b); $c->toDecimalString(); // '0.3' — exact, always
Arithmetic is exact at every step. The result serialises as a decimal string ('0.3'), not a float, so API consumers and hash-based fiscal systems (AEAT Verifactu, etc.) always receive the canonical value.
Requirements
- PHP 8.3 or 8.4
- Laravel 12.x or 13.x
brick/math^0.14.2 (pulled in automatically)
Installation
composer require aichadigital/lara100
No configuration file is required — lara100 ships with no publishable config.
Usage
1. Database schema
Store unscaled integers. The scale is declared at the model level, not in the column:
Schema::create('products', function (Blueprint $table) { $table->id(); $table->integer('price')->default(0); // scale 2: 1999 = 19.99 $table->integer('exchange_rate')->default(0); // scale 4: 12345 = 1.2345 $table->timestamps(); });
2. Model casts
Declare the cast with the scale as a colon-separated suffix. The scale is required; omitting it throws immediately rather than silently misinterpreting the column:
use AichaDigital\Lara100\Casts\FixedDecimalCast; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected function casts(): array { return [ 'price' => FixedDecimalCast::class.':2', // 1999 → 19.99 'exchange_rate' => FixedDecimalCast::class.':4', // 12345 → 1.2345 ]; } }
3. Reading and writing
The cast accepts only FixedDecimal|null on assignment. Scalars are rejected deliberately — callers must choose the conversion explicitly:
// Reading: the cast returns a FixedDecimal built from the stored integer $product = Product::find(1); $price = $product->price; // FixedDecimal('19.99', scale=2) $price->toDecimalString(); // '19.99' $price->unscaledValue(); // 1999 $price->scale(); // 2 echo json_encode($price); // "19.99" (exact decimal string, not float) // Writing: build a FixedDecimal explicitly, then assign it $product->price = FixedDecimal::ofDecimalString('29.99', 2); $product->save(); // DB stores 2999 (integer) // null is allowed for nullable columns $product->price = null; $product->save(); // DB stores NULL
Assigning a raw scalar throws InvalidFixedDecimal — the strict contract prevents accidental precision loss at the boundary:
$product->price = 19.99; // throws InvalidFixedDecimal — use ofDecimalString / ofFloat
4. Constructing FixedDecimal
Recommended constructors:
use AichaDigital\Lara100\ValueObjects\FixedDecimal; // From the raw stored integer (no conversion, no rounding — the cleanest path) $price = FixedDecimal::ofUnscaled(1999, 2); // 19.99 $rate = FixedDecimal::ofUnscaled(12345, 4); // 1.2345 // From a decimal string (parse and optionally coerce to a target scale) $price = FixedDecimal::ofDecimalString('19.99'); // scale inferred from the string's decimals (here, 2) $price = FixedDecimal::ofDecimalString('19.9', 2); // coerced to scale 2 → '19.90' // Zero at a given scale $zero = FixedDecimal::zero(2); // 0.00
Migration-only boundary (not for new code):
use AichaDigital\Lara100\RoundingMode; // From a float — reintroduces IEEE-754 at the boundary; use only when crossing // a legacy float API or when receiving user input that cannot be treated as a string $price = FixedDecimal::ofFloat(19.99, 2, RoundingMode::HalfUp);
5. Arithmetic
All arithmetic produces a new FixedDecimal; the original is unchanged (immutable):
$price = FixedDecimal::ofDecimalString('19.99'); $tax_rate = FixedDecimal::ofDecimalString('0.21'); $tax = $price->multipliedBy($tax_rate)->toScale(2, RoundingMode::HalfUp); $total = $price->plus($tax); $tax->toDecimalString(); // '4.20' $total->toDecimalString(); // '24.19' // Division always requires an explicit scale and rounding mode $half = $total->dividedBy(2, 2, RoundingMode::HalfUp); $half->toDecimalString(); // '12.10'
Available arithmetic: plus, minus, multipliedBy, dividedBy, toScale, negated, abs.
6. Comparison
$a = FixedDecimal::ofDecimalString('10.00'); $b = FixedDecimal::ofDecimalString('20.00'); $a->isEqualTo($b); // false $a->isLessThan($b); // true $a->isGreaterThan($b); // false $a->isZero(); // false $a->isPositive(); // true $a->isNegative(); // false $a->compareTo($b); // -1
7. Output
$price = FixedDecimal::ofUnscaled(1999, 2); $price->toDecimalString(); // '19.99' — exact, preferred for persistence / hashing $price->unscaledValue(); // 1999 — raw stored integer $price->scale(); // 2 $price->toFloat(); // 19.99 — reintroduces float imprecision; display/legacy only $price->toBigDecimal(); // Brick\Math\BigDecimal — escape hatch for raw brick/math use json_encode($price); // "19.99" — jsonSerialize() returns the decimal string
8. RoundingMode
lara100 ships its own RoundingMode enum — fully encapsulated from the underlying brick/math engine:
| Case | Description | Typical use |
|---|---|---|
HalfUp |
0.5 rounds away from zero | EU/Spain fiscal standard; default |
HalfEven |
0.5 rounds to nearest even | Banker's rounding; accounting |
HalfDown |
0.5 rounds toward zero | Conservative rounding |
Up |
Always away from zero | Always round up |
Down |
Always toward zero (truncate) | Always truncate |
Ceiling |
Toward positive infinity | |
Floor |
Toward negative infinity |
HalfUp is the European/Spanish fiscal standard and the default wherever lara100 accepts a rounding mode. HalfEven (banker's rounding) is the right choice for general accounting aggregations.
Positioning
lara100 is a Laravel-native, scale-aware exact decimal built on top of brick/math. It occupies a specific niche — here is when to reach for each option:
| Library | Abstraction | Laravel cast | Currency | When to use |
|---|---|---|---|---|
| lara100 | FixedDecimal (exact, scale-aware) |
Yes (FixedDecimalCast) |
No | Prices, rates, fiscal amounts in Laravel — exact arithmetic without currency overhead |
brick/math |
BigDecimal, BigInteger |
No | No | Raw exact arithmetic outside Laravel; lara100 builds on this |
brick/money |
Money (amount + currency) |
No | Yes | Multi-currency systems, allocation, currency conversion |
moneyphp/money |
Money (amount + currency) |
No | Yes | Same as brick/money; wider ecosystem adoption |
Choose lara100 when:
- You are building a Laravel application and want Eloquent models to carry
FixedDecimalattributes directly - You need exact arithmetic over scale-aware values (prices at scale 2, VAT rates at scale 4, etc.)
- You do not need multi-currency support or allocation algorithms
Reach for brick/math directly when:
- You need exact arithmetic outside of Laravel (CLI tools, pure PHP libraries, etc.)
- You need
BigIntegerorBigRational(lara100 only wrapsBigDecimal)
Reach for brick/money or moneyphp/money when:
- You need currency codes, currency conversion, or monetary allocation (splitting a total into N parts that sum exactly)
lara100 encapsulates brick/math ^0.14.2 internally. The dependency is lightweight; brick types are not exposed except via the explicit toBigDecimal() escape hatch.
Comparison with Alternatives
| Solution | DB column | PHP value | Precision | Laravel cast | Multi-currency |
|---|---|---|---|---|---|
lara100 FixedDecimal |
INTEGER (unscaled) |
FixedDecimal (exact) |
Exact | Yes | No |
lara100 Base100Int |
INTEGER (cents) |
int |
Exact | Yes | No |
brick/math (BigDecimal) |
— | BigDecimal (exact) |
Exact | No | No |
brick/money |
INTEGER |
Money (exact + currency) |
Exact | No | Yes |
moneyphp/money |
INTEGER |
Money (exact + currency) |
Exact | No | Yes |
Native DECIMAL column |
DECIMAL(10,2) |
float |
Float — imprecise | No | No |
Testing
composer test
composer test-coverage
composer test-parallel
Code Quality
composer phpstan composer pint
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Security Vulnerabilities
If you discover a security vulnerability, please send an e-mail to Abdelkarim Mateos Sanchez via abdelkarim.mateos@castris.com.
Credits
License
The MIT License (MIT). Please see License File for more information.
About AichaDigital
AichaDigital is an IT company focused on IT services.