A small, framework-agnostic PHP money value object for safe monetary calculations.

Maintainers

Package info

github.com/leancaptain/money

pkg:composer/leancaptain/money

Transparency log

Statistics

Installs: 22

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.3 2026-08-11 11:12 UTC

This package is auto-updated.

Last update: 2026-08-11 16:30:13 UTC


README

A small, framework-agnostic PHP money value object for representing, comparing, and calculating monetary values safely using integer minor units.

The package is intentionally lightweight and has no runtime dependencies.

Requirements

  • PHP 8.4+

Installation

Install via Composer:

composer require leancaptain/money

Quick Start

use LeanCaptain\Money\Currency;
use LeanCaptain\Money\Money;

$money = Money::of('1250.50', Currency::BDT);

$money->minorAmount(); // 125050
$money->toDecimal();   // "1250.50"
$money->currency();    // Currency::BDT

Monetary amounts should be provided as integers or decimal strings rather than floats.

Money::of('19.99', Currency::USD);
Money::of(100, Currency::BDT);

Minor Units

Money is stored internally using integer minor units.

$money = Money::of('125.50', Currency::BDT);

$money->minorAmount(); // 12550

You can also construct money directly from minor units:

$money = Money::fromMinor(12550, Currency::BDT);

$money->toDecimal(); // "125.50"

Currencies determine their own minor-unit precision.

Currency::BDT->minorUnit(); // 2
Currency::USD->minorUnit(); // 2
Currency::JPY->minorUnit(); // 0

Built-in Currencies

The initial release intentionally provides a small currency set:

  • BDT
  • USD
  • EUR
  • GBP
  • JPY

Applications are not required to use the built-in Currency enum.

Custom Currencies

Implement CurrencyContract to provide an application-specific currency:

use LeanCaptain\Money\Contracts\CurrencyContract;

enum AppCurrency: string implements CurrencyContract
{
    case BDT = 'BDT';
    case USD = 'USD';

    public function code(): string
    {
        return $this->value;
    }

    public function minorUnit(): int
    {
        return 2;
    }
}

Then use it normally:

$money = Money::of('100.00', AppCurrency::BDT);

Currency compatibility is determined by both the currency code and minor-unit precision.

Arithmetic

Addition

$total = Money::of('100.50', Currency::BDT)
    ->add(Money::of('50.25', Currency::BDT));

$total->toDecimal(); // "150.75"

Subtraction

$balance = Money::of('100.00', Currency::BDT)
    ->subtract(Money::of('25.50', Currency::BDT));

$balance->toDecimal(); // "74.50"

Multiplication

$result = Money::of('100.00', Currency::BDT)
    ->multiply('1.15');

$result->toDecimal(); // "115.00"

Division

$result = Money::of('100.00', Currency::BDT)
    ->divide(4);

$result->toDecimal(); // "25.00"

Arithmetic operations return new Money instances. Existing instances are never mutated.

Rounding

The default rounding behavior is intentionally strict.

If a value cannot be represented exactly using the currency's minor-unit precision, a RoundingRequired exception is thrown.

use LeanCaptain\Money\Exceptions\RoundingRequired;

Money::of('10.999', Currency::BDT);
// throws RoundingRequired

Rounding can be requested explicitly:

use LeanCaptain\Money\RoundingMode;

$money = Money::of(
    '10.999',
    Currency::BDT,
    RoundingMode::HALF_UP,
);

$money->toDecimal(); // "11.00"

Supported rounding modes:

  • RoundingMode::REJECT
  • RoundingMode::HALF_UP
  • RoundingMode::DOWN

The same rules apply to calculations that require rounding:

$result = Money::of('10.01', Currency::BDT)
    ->divide(2, RoundingMode::HALF_UP);

$result->toDecimal(); // "5.01"

Comparisons

$first = Money::of('100.00', Currency::BDT);
$second = Money::of('50.00', Currency::BDT);

$first->equals($second);               // false
$first->isGreaterThan($second);        // true
$first->isGreaterThanOrEqual($second); // true
$first->isLessThan($second);           // false
$first->isLessThanOrEqual($second);    // false

You can also inspect the state of an amount:

$money->isZero();
$money->isPositive();
$money->isNegative();

Ordering or arithmetic between incompatible currencies throws a CurrencyMismatch exception.

Money::of('100', Currency::BDT)
    ->add(Money::of('100', Currency::USD));

// throws CurrencyMismatch

The package does not perform currency conversion.

Other Operations

Negate an amount:

Money::of('100', Currency::BDT)
    ->negate()
    ->toDecimal();

// "-100.00"

Get the absolute value:

Money::of('-100', Currency::BDT)
    ->absolute()
    ->toDecimal();

// "100.00"

Money Factory

MoneyFactory can be used when an application has default currency and rounding requirements.

use LeanCaptain\Money\Currency;
use LeanCaptain\Money\MoneyFactory;
use LeanCaptain\Money\RoundingMode;

$factory = new MoneyFactory(
    Currency::BDT,
    RoundingMode::HALF_UP,
);

$money = $factory->of('100.50');

This avoids repeating application defaults while keeping Money itself free from global configuration.

You can also construct values from minor units:

$money = $factory->fromMinor(10050);

$money->toDecimal(); // "100.50"

String Representation

Money implements Stringable.

$money = Money::of('1250.50', Currency::BDT);

(string) $money;
// "1250.50 BDT"

The string representation is intentionally predictable and locale-independent.

Locale-aware presentation formatting should be handled separately by the consuming application.

JSON Serialization

Money implements JsonSerializable.

$money = Money::of('1250.50', Currency::BDT);

json_encode($money);

Produces:

{
    "amount": "1250.50",
    "minor_amount": 125050,
    "currency": "BDT"
}

The decimal amount is serialized as a string to avoid introducing floating-point representation into the monetary value.

Exceptions

The package provides dedicated exceptions for common invalid operations:

InvalidMoneyAmount

Thrown when an amount has invalid syntax.

Money::of('invalid', Currency::BDT);

RoundingRequired

Thrown when an otherwise valid value cannot be represented exactly and the rounding mode is REJECT.

Money::of('10.999', Currency::BDT);

CurrencyMismatch

Thrown when arithmetic or ordering is attempted between incompatible currencies.

Money::of('100', Currency::BDT)
    ->add(Money::of('100', Currency::USD));

Division by zero throws PHP's native DivisionByZeroError.

Design Principles

LeanCaptain Money intentionally follows a few simple rules:

  • Monetary values are stored as integer minor units.
  • Core monetary operations do not use floating-point arithmetic.
  • Currency precision is explicit.
  • Rounding is explicit when required.
  • Money values are immutable.
  • Different currencies cannot accidentally be combined.
  • Consumer applications may provide their own currencies.
  • The core package is framework-agnostic.
  • The package has no runtime dependencies.

The goal is to provide a small, predictable money primitive rather than a complete financial or accounting framework.

Testing

Run the test suite with:

composer test

License

LeanCaptain Money is open-sourced software licensed under the MIT license.