LaravelPackages.net
Acme Inc.
Toggle sidebar
aichadigital/lara100

Laravel cast for handling decimal values as base-100 integers (cents/centesimals)

2.444
0
v2.0.0
About aichadigital/lara100

aichadigital/lara100 is a Laravel package for laravel cast for handling decimal values as base-100 integers (cents/centesimals). It currently has 0 GitHub stars and 2.444 downloads on Packagist (latest version v2.0.0). Install it with composer require aichadigital/lara100. Discover more Laravel packages by aichadigital or browse all Laravel packages to compare alternatives.

Last updated

Lara100 — Exact Decimal for Laravel

Latest Version Total Downloads Pipeline Coverage PHPStan level max PHP Version Laravel Version License

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 FixedDecimal attributes 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 BigInteger or BigRational (lara100 only wraps BigDecimal)

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 [email protected].

Credits

License

The MIT License (MIT). Please see License File for more information.

About AichaDigital

AichaDigital is an IT company focused on IT services.

Comments