aichadigital/lara100
Lara100 — Exact Decimal for Laravel
Development happens on gitlab.castris.com. The GitHub repository is a read-only distribution mirror: issues and pull requests opened there are not seen.
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.
Related Packages
Laravel Eloquent casts for PHP 8.4's BCMath\Number object API, enabling natural...
Object-oriented wrapper/shim for BC Math PHP extension. Allows for arbitrary-pre...
Add a short way to create accessors and mutators for decimal fields
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v2.0.0 | ^8.3| | ^12.0| | MIT | |
| v1.3.0 | ^8.3| | ^12.0| | MIT | |
| v1.2.1 | ^8.3| | ^12.0| | MIT | |
| v1.2.0 | ^8.3| | ^12.0| | MIT | |
| v1.1.0 | ^8.3| | ^11.0| | MIT | |
| v1.0.2 | ^8.3| | ^11.0| | MIT | |
| v1.0.1 | ^8.3| | ^11.0| | MIT | |
| v1.0.0 | ^8.3| | ^11.0| | MIT |