elriseio/finance-money-bundle

Type-safe monetary value objects, currency registry, exchange-rate port, and Symfony Bundle integration for high-load financial applications.

Maintainers

Package info

github.com/elriseio/finance-money-bundle

Documentation

Type:symfony-bundle

pkg:composer/elriseio/finance-money-bundle

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.2 2026-07-28 08:10 UTC

This package is auto-updated.

Last update: 2026-07-28 08:31:07 UTC


README

Type-safe monetary value objects, currency registry, exchange-rate port, and Symfony Bundle integration for high-load financial applications.

CI Latest Stable Version Total Downloads License

Money Bundle is a Symfony-native bundle for high-load financial applications. It provides a type-safe monetary value object layer for PHP 8.3+ applications. Every arithmetic operation goes through BCMath; currency mismatch is a compile-time error; the exchange-rate port is injected through DI so consumers stay in control of regional API choice.

The bundle is part of the elriseio/ vendor namespace alongside the sibling elriseio/dbal-bundle. The composer name elriseio/finance-money-bundle is the canonical identifier on Packagist and in the project's composer.json name field.

What you get

  • Money value object — immutable, final readonly, BCMath-backed. Static factories accept a string code or Currency instance; arithmetic (plus, minus, multipliedBy, dividedBy, allocate, compare) returns new instances. Optional cross-currency comparison via Money::compare(other, ?provider) (DE-020).
  • Currency value object — code, scale, type (Fiat / Crypto / Custom), symbol, name.
  • Decimal engine — BCMath-only facade. Direct bcdiv / bcmod / bcscale outside Decimal\Math is forbidden by a custom PHPStan rule (ForbiddenBcFunctionInDecimalRule).
  • FundConverter — float ↔ string ↔ int ↔ fund with externalMultiplier for PSP cents / satoshi / virtual-currency units.
  • ExchangeRateProviderInterface — DI-only contract; concrete providers (CbrRateProvider, EcbRateProvider, …) are not part of this bundle's core. They live in optional companion packages. Provider metadata (source, at) is exposed through the rateWithMetadata() method (DE-021); the simpler rate() method stays for backwards compatibility.
  • ConversionResult DTO — carries (from, to, rate, source, at) for conversion audit (DE-021). Returned by Money::convertWithMetadata() (opt-in sibling method).
  • CurrencyRegistryInterface — registry of currencies with soft-fail lookup (tryGet returns null; get throws UnknownCurrencyException). The bundle ships no default registry; host applications populate the registry at boot time via CurrencyRegistry::mutable()->register(...) (or CurrencyRegistry::fromCatalogue(...) for an immutable seed).
  • InMemoryExchangeRateProvider — a stub provider shipped in the core for tests; never use in production.
  • Symfony Bundleelrise_finance_money config key, services auto-wired, attribute-based registration (#[AsCurrency]), ValidateConfigurationPass compiler pass.
  • Strict exception taxonomy — every public method has a declared @throws set; domain exceptions carry an errorCode() for structured logging.

When to use this

  • Multi-currency fintech applications: iGaming, Banking, Lending, Crypto, PSP, Neobank.
  • Any system where BCMath is non-negotiable and float drift is unacceptable.
  • Symfony 7.x or 8.x projects that want first-class monetary contracts without giving up framework independence for the core.
  • Codebases that need Money / Currency value objects but want consumer code to not import BCMath directly.

When NOT to use this

  • Mono-currency apps where int cents is enough — the Money value object adds overhead you do not need.
  • High-throughput hot paths (> 10k ops/sec) where the BCMath cost matters more than type safety — use a native int representation and store amounts in minor units.
  • Pure presentation work — the bundle's MoneyFormatter uses \NumberFormatter via ext-intl. For non-monetary display, use NumberFormatter directly.

Requirements

Requirement Version Mandatory
PHP 8.3, 8.4, or 8.5 yes
ext-bcmath any yes
ext-intl any optional (used by MoneyFormatter)
Composer 2.x yes
Symfony 7.x or 8.x yes (for the Bundle layer; core is framework-agnostic)

PHP 8.2 is not supported (the bundle uses final readonly class, enum cases, and typed constants).

Installation

composer require elriseio/finance-money-bundle

Register the bundle in config/bundles.php (Symfony Flex does this automatically):

return [
    // ...
    Elrise\Finance\Bundle\Money\FinanceMoneyBundle::class => ['all' => true],
];

Quick start

use Elrise\Finance\Bundle\Money\Money;
use Elrise\Finance\Bundle\Money\Currency\Currency;
use Elrise\Finance\Bundle\Money\Currency\CurrencyRegistry;

$registry = CurrencyRegistry::mutable([
    Currency::iso('USD', '840', null, 'US Dollar'),
    Currency::iso('EUR', '978', null, 'Euro'),
    Currency::crypto('BTC', 8, null, 'Bitcoin'),
    Currency::crypto('ETH', 18, null, 'Ether'),
    Currency::custom('POINTS', 0, null, 'Bonus Points'),
]);
$usd = $registry->get('USD');

$deposit = Money::of('100.00', $usd);
$bonus   = $deposit->multipliedBy('1.10');         // '110.00'
$total   = $deposit->plus($bonus);                  // '110.00 USD'
$fmt     = $total->format('ru_RU');                 // '110.00 USD'

// Cross-currency conversion with metadata (DE-021)
$result = $total->convertWithMetadata(
    'EUR',
    new \DateTimeImmutable(),
    $rateProvider,
);
// $result->to()      === Money(95.85, EUR)
// $result->rate()    === '0.9585'
// $result->source()  === 'ecb' (provider-supplied)
// $result->at()      === DateTimeImmutable(...)

// Cross-currency comparison (DE-020)
$sign = $usdMoney->compare($eurMoney, $rateProvider);
// -1 / 0 / 1

A minimal Symfony services.yaml excerpt:

services:
    Elrise\Finance\Bundle\Money\Contract\ExchangeRateProviderInterface:
        alias: 'app.exchange_rate.ecb_provider'   # from your project

    Elrise\Finance\Bundle\Money\CurrencyRegistry:
        arguments:
            $currencies: '%elrise_finance_money.currencies%'

    _instanceof:
        Elrise\Finance\Bundle\Money\Contract\CurrencyInterface:
            tags: ['elrise_finance_money.currency']

Configuration

elrise_finance_money:
    enabled: true

    default_currency: USD
    default_multiplier: 100000   # internal precision (5 fractional digits)
    bcmath_scale: 8             # safety limit for BCMath operations
    view_precision: 2           # precision for UI formatting
    rounding_mode: half_up       # half_up | half_even | half_down

    # The bundle ships no default currency registry. Host
    # applications populate the registry at boot time via
    # CurrencyRegistry::mutable()->register(...). The currency
    # metadata below documents the recommended scale/type per
    # code; symbols are consumer-supplied at the UI edge.

    exchange_rate_provider: 'app.exchange_rate.ecb_provider'

Quality gates

Run the full battery locally before opening a PR:

composer test   # PHPUnit (Unit / Contract / Property-based)
composer stan   # PHPStan level 8 + ergebnis rules
composer cs     # PHPCS (PSR-12 + custom Money standard)
composer bench  # phpbench micro-benchmarks

All four commands exit non-zero on failure. CI runs the same matrix on PHP 8.3, 8.4, and 8.5; Composer resolves symfony/* to the highest stable branch that matches the platform PHP (Symfony 7.x on PHP 8.3, Symfony 7.x or 8.x on PHP 8.4, Symfony 8.x on PHP 8.5).

Custom PHPCS sniffs (tests/Rules/Money/):

  • Money.HotPath.NoFloatOnMoneyHotPath — fails on float parameter, return type, or property under src/Money.php, src/Currency/, or src/Decimal/.
  • Money.HotPath.ImmutabilityGuard — fails if a value object in src/Money.php, src/Currency/Currency.php, or src/Decimal/Decimal.php is missing final or readonly, declares a non-readonly property, or exposes a public setter.

Custom PHPStan rule:

  • ForbiddenBcFunctionInDecimalRule — blocks direct bcdiv, bcmod, bcscale calls in Decimal\ outside Decimal\Math.

Testing

Test suite Where Purpose
Unit tests/Unit/ Pure unit tests, no Symfony container
Contract tests/Contract/ Public-interface invariants; cross-implementation consistency
Property-based tests/Property/ Round-trip and invariant fuzzing (Eris)
Integration itests/ End-to-end with real Symfony container, in-memory rate provider, scenario runners
Benchmark bench/ phpbench micro-benchmarks for hot-path budgets
Failure-mode itests/FailureMode/ Cache-down, provider-down, BCMath-scale-overflow

Property-based tests assert algebraic invariants like add(a, b) == add(b, a), multiply(a, 1) == a, allocate(n) sums to original, and round-trip fromCanonicalString(toCanonicalString(x)) == x.

Performance

The hot path (Money::of, Money::plus, Money::minus, Money::multipliedBy) stays under documented budgets:

Operation Budget Notes
Money::of(string, Currency) < 100 µs includes Decimal construction
Money::plus(Money) < 50 µs BCMath bcadd at receiver's scale
Money::multipliedBy(string) < 100 µs includes quantization
Money::convert(Currency, …) < 1 ms with cached provider, single-hop
Money::convertWithMetadata(…) < 1.2 ms adds ConversionResult construction
Money::compare(Money, …) < 200 µs single-currency baseline
Money::compare(other, provider) < 1.2 ms cross-currency, with provider call
Money::allocate(int) < 1 ms largest-remainder distribution
Money::format(string) < 500 µs with \NumberFormatter
Money::toFloat() < 50 µs one bcdiv cast

Budgets are tracked via phpbench; regressions fail CI. See bench/ for the exact scenarios.

Production deployment

  • Runtime requirements are the same as ## Requirements above. No additional PHP extensions, no native libraries.
  • Container compilation validates config at build time via ValidateConfigurationPass. Misconfiguration is caught in CI, not in production.
  • No floating-point state. Every amount is a BCMath-precise decimal string. Floats appear only as transient parse targets.
  • Deterministic rounding. HALF-UP by default; rounding mode is configurable globally. Same input produces the same output across PHP versions and platforms.
  • Exchange rates are not bundled. Consumers must implement ExchangeRateProviderInterface and register it via DI. The bundle ships an in-memory provider for tests but never for production.
  • Logs and metrics. Public exceptions carry errorCode() for structured logging; no bundle code emits PII or financial values to logs.

Compatibility matrix

PHP Symfony BCMath Status
8.3 7.x required supported
8.4 7.x required supported
8.4 8.x required supported
8.5 8.x required supported
8.2 any not supported

Roadmap

Active wave: Wave 0 — Foundation and Contract Baseline (see docs/ROADMAP.md for the full sequence). The roadmap is reviewed after each closed implementation task.

Production-readiness status is tracked in docs/PRODUCTION_READINESS_CHECKLIST.md.

Architecture at a glance

                        ┌────────────────────────────────┐
                        │  elriseio/finance-money-bundle │
                        │  (this package, framework-    │
                        │   agnostic core + Symfony     │
                        │   Bundle integration)         │
                        └─────────────┬──────────────────┘
                                      │
                ┌─────────────────────┼─────────────────────┐
                │                     │                     │
        ┌───────▼────────┐    ┌────────▼────────┐    ┌───────▼────────┐
        │ Money /        │    │ Decimal\Math    │    │ CurrencyReg.  │
        │ Currency VO    │    │ (BCMath facade) │    │ (ISO + custom)│
        └───────┬────────┘    └────────┬────────┘    └───────┬────────┘
                │                     │                     │
                └─────────────────────┼─────────────────────┘
                                      │
                        ┌─────────────▼──────────────────┐
                        │  Symfony DI / Bundle            │
                        │  (services, compiler passes,    │
                        │   config validation)            │
                        └─────────────┬──────────────────┘
                                      │
                        ┌─────────────▼──────────────────┐
                        │  Sub-packages (Doctrine, Cycle, │
                        │  Serializer, Forms, Exchange-   │
                        │  Rate providers) — optional,    │
                        │  per consumer                   │
                        └────────────────────────────────┘

For component-by-component documentation, see docs/components/. For public-contract documentation, see docs/contracts/. For architectural decision records, see docs/adr/.

Contributing

See CONTRIBUTING.md. Notable requirements:

  • PHPStan level 8 over src/, tests/, bench/.
  • PHPCS PSR-12 + the Money custom standard.
  • New public contracts require an ADR before implementation.
  • Changes to the arithmetic core require a property-based test covering the new invariant.
  • Production-readiness checklist (docs/PRODUCTION_READINESS_CHECKLIST.md) must stay green before a release tag.
  • No references to internal production projects, vendor paths, or private repositories in public docs (sanitization contract).

Security

  • No PII handling. The bundle operates on amounts and codes; no customer identifiers, no card data, no account numbers.
  • No secrets in code. API keys for rate providers belong in the consumer's secret store (Vault, Symfony secrets, AWS Secrets Manager, …), injected via environment variables.
  • BCMath safety limit is set per configuration; exceeding it raises MathScaleException rather than silently truncating.
  • CSPRNG is not used by the bundle (no token generation).

Report security issues privately — see the GitHub Security tab.

Acknowledgements

This bundle consolidates patterns from four production fintech systems that have run since 2018. The architecture rationale is documented in docs/architecture.md. The acknowledgements section is intentionally generic — no internal project names are exposed in public documentation per the sanitization contract.

More examples and more docs

Working examples, end-to-end tutorials, provider recipes, and architectural deep-dives live on the project page:

elrise.io/projects/finance-money-bundle

If something is missing on that page, open an issue on GitHub and tag it docs.

License

MIT — see LICENSE.