PHPackages                             syriable/laravel-ledger - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. syriable/laravel-ledger

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

syriable/laravel-ledger
=======================

An immutable, append-only, double-entry financial ledger engine for Laravel.

v0.9.1(2w ago)12MITPHPPHP ^8.3CI passing

Since May 21Pushed 2w agoCompare

[ Source](https://github.com/syriable/laravel-ledger)[ Packagist](https://packagist.org/packages/syriable/laravel-ledger)[ Docs](https://github.com/syriable/laravel-ledger)[ RSS](/packages/syriable-laravel-ledger/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (9)Versions (7)Used By (0)

    ![SyriableLedger Logo](art/ledger-light.svg)

[![Latest Version on Packagist](https://camo.githubusercontent.com/4d43963633742969465d26f2f224dfacbcd2d1092d9a0a9055f6173408fe3f9d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7379726961626c652f6c61726176656c2d6c65646765722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-ledger)[![GitHub Tests Action Status](https://camo.githubusercontent.com/f5f537cb957a876e917222c5e37f7a7d0896f15d5f4528aeba1ae153127a5755/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c65646765722f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-ledger/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/a2613f71451e462dfe16b4ff26c666d93eddecab6df2d94ac40019288a4671b1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c65646765722f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-ledger/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![GitHub PHPStan Action Status](https://camo.githubusercontent.com/c154d8facfd297164aa1886621ee82ef867ac487fad93ad845f3e5ec9bd51170/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c65646765722f7068707374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-ledger/actions?query=workflow%3A%22PHPStan%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/951e28d6d2c6b4e8cebffecf428a122566e01678d5b812e6b067e31d64d8e189/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7379726961626c652f6c61726176656c2d6c65646765722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-ledger)

An immutable, append-only, double-entry financial ledger engine for Laravel. Strongly opinionated, minimal core, strict invariants. It records balanced double-entry transactions atomically and idempotently, and reads them back accurately — and it refuses to do anything that would let your books drift.

```
use Syriable\Ledger\Facades\Ledger;

Ledger::createLedger(slug: 'platform-main', currency: 'USD');

$result = Ledger::post(new OrderPaidPosting($order));

$result->transaction;   // the recorded Transaction
$result->wasReplayed;   // true if this reference was already posted
```

> **Status:** `1.0.0-rc.1` — API frozen for the soak period; behaviour will not change before `1.0.0` unless a critical bug demands it. See [CHANGELOG](CHANGELOG.md) and [UPGRADING](UPGRADING.md).

Requirements
------------

[](#requirements)

- PHP **8.3** or higher (64-bit)
- Laravel **11**, **12**, or **13**
- One of: **PostgreSQL 12+**, **MySQL 8+**, **MariaDB 10.4+**, or **SQLite 3.31+**

The full CI matrix runs against PostgreSQL 16, MySQL 8, and SQLite on PHP 8.3 and 8.4.

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require syriable/laravel-ledger
```

Publish and run the migrations:

```
php artisan vendor:publish --tag="ledger-migrations"
php artisan migrate
```

Optionally publish the config file:

```
php artisan vendor:publish --tag="ledger-config"
```

The full config reference (every key, type, and default) lives in [`docs/05-installation-and-quickstart.md`](docs/05-installation-and-quickstart.md#configuration).

Usage
-----

[](#usage)

The public surface is three verbs: **open**, **post**, **reverse**.

### Open a ledger and accounts

[](#open-a-ledger-and-accounts)

```
use Syriable\Ledger\Enums\AccountType;
use Syriable\Ledger\Facades\Ledger;

Ledger::createLedger(slug: 'platform-main', currency: 'USD');

$cash = Ledger::for('platform-main')->openAccount(
    code: 'platform.cash.usd',
    type: AccountType::Asset,
    currency: 'USD',
);

$revenue = Ledger::for('platform-main')->openAccount(
    code: 'platform.revenue.usd',
    type: AccountType::Revenue,
    currency: 'USD',
);
```

### Define a Posting

[](#define-a-posting)

A `Posting` is the one and only way to write to the ledger. It is a deterministic domain operation that produces a balanced transaction.

```
php artisan make:posting OrderPaidPosting
```

```
use Syriable\Ledger\Data\EntryDraft;
use Syriable\Ledger\Models\Account;
use Syriable\Ledger\Postings\Posting;
use Syriable\Ledger\ValueObjects\Money;
use Syriable\Ledger\ValueObjects\Reference;

final class OrderPaidPosting extends Posting
{
    /**
     * Accounts and amounts are resolved by the caller and passed in.
     * A Posting must never query the database inside entries() — that
     * would break determinism on retry. See docs/04-the-posting-contract.md.
     */
    public function __construct(
        private readonly string $orderId,
        private readonly Account $cash,
        private readonly Account $revenue,
        private readonly Money $total,
    ) {}

    public function ledger(): string       { return 'platform-main'; }
    public function currency(): string     { return $this->total->currency; }
    public function reference(): Reference  { return Reference::for('order.paid', $this->orderId); }

    public function entries(): array
    {
        return [
            EntryDraft::debit($this->cash, $this->total),
            EntryDraft::credit($this->revenue, $this->total),
        ];
    }
}
```

### Post it

[](#post-it)

```
$scope   = Ledger::for('platform-main');
$cash    = $scope->account('platform.cash.usd');
$revenue = $scope->account('platform.revenue.usd');

$result = Ledger::post(new OrderPaidPosting(
    orderId: $order->id,
    cash: $cash,
    revenue: $revenue,
    total: Money::of(9_900, 'USD'),
));
```

Posting is idempotent on the `Reference`. Posting the same operation twice returns the original transaction with `wasReplayed = true` — no duplicate write.

### Reverse it

[](#reverse-it)

```
Ledger::reverse($result->transaction, reason: 'chargeback');
```

A reversal is a new, immutable transaction that inverts the original. A transaction can be reversed at most once, and a reversal cannot itself be reversed — both enforced at the database level. For partial refunds, post a new operation rather than reversing.

### Read balances

[](#read-balances)

```
$cash = Ledger::for('platform-main')->account('platform.cash.usd');

$cash->balance();              // int — signed balance (negative = overdraft)
$cash->balanceMoney();         // Money
$cash->balanceAsOf($moment);   // int — historical balance, from entries
$cash->entries;                // immutable history
```

When you iterate accounts and call `balance()` on each, use the `withBalance` scope to eager-load the projection and avoid an N+1:

```
$accounts = Account::query()
    ->withBalance()
    ->where('ledger_id', $ledgerId)
    ->get();

$accounts->each(fn (Account $a) => $a->balance());  // zero extra queries
```

### Owner-side ergonomics

[](#owner-side-ergonomics)

Apply the `HasAccounts` trait to any model that owns accounts:

```
use Syriable\Ledger\HasAccounts;

class User extends Model
{
    use HasAccounts;

    public function defaultLedgerSlug(): string
    {
        return 'platform-main';
    }
}

$user->openAccount('available.usd', AccountType::Liability, 'USD');
$user->account('available.usd')->balance();
```

### Verify integrity

[](#verify-integrity)

```
php artisan ledger:verify                    # all ledgers
php artisan ledger:verify --ledger=platform-main
php artisan ledger:rebuild-balances          # rebuild projections from entries
php artisan ledger:simulate                  # rehearse a marketplace at volume
```

`ledger:verify` checks that every transaction balances, every ledger is zero-sum, and every balance projection matches the entries. It exits non-zero on drift — wire it into your scheduler and your CI.

`ledger:simulate` drives a realistic marketplace lifecycle through the real API at volume and verifies the result against an independent shadow ledger — a one-command way to stress-test the package before trusting it with real money. Run it only against a disposable database, and run `php artisan migrate:fresh` before each run to avoid reference collisions with previous runs. See [`docs/09-operations.md`](docs/09-operations.md#rehearsing-a-deployment-with-ledgersimulate).

Core invariants
---------------

[](#core-invariants)

These are enforced by validators and database constraints. They cannot be relaxed by configuration.

- **Append-only** — no financial column on `transactions` or `entries` is ever mutated.
- **Always balanced** — `Σ debits == Σ credits` per transaction.
- **Single currency per transaction** — FX is two linked postings.
- **Entry currency matches account currency.**
- **No cross-ledger entries.**
- **Amounts are positive integers** — direction is encoded by Debit/Credit.
- **Every transaction has a unique idempotency reference.**
- **Archived accounts reject new entries** (reversals exempted).
- **A transaction can be reversed at most once; reversals cannot be reversed.**
- **No soft-deletes** on financial models.
- **Money never crosses the float boundary.**

See [`docs/03-invariants.md`](docs/03-invariants.md) for the full list and what enforces each one.

Documentation
-------------

[](#documentation)

Full documentation lives in [`docs/`](docs/) — start with the [documentation index](docs/README.md). If you have never worked with a double-entry ledger, read [**The Posting Contract &amp; Direction of Value**](docs/04-the-posting-contract.md) first.

**Start here**

- [Introduction](docs/01-introduction.md) — why this package exists.
- [Concepts](docs/02-concepts.md) — the seven concepts the package is built from.
- [Invariants](docs/03-invariants.md) — the financial rules and what enforces each.

**Build with the package**

- [The Posting Contract](docs/04-the-posting-contract.md) — debit/credit direction and the determinism rules.
- [Installation &amp; Quickstart](docs/05-installation-and-quickstart.md) — the full lifecycle in five minutes.
- [Postings Cookbook](docs/06-postings-cookbook.md) — a marketplace, worked end to end.
- [Reversals vs Refunds](docs/07-reversals-and-refunds.md) — the distinction that causes the most bugs.
- [Balances](docs/08-balances.md) — projection vs aggregation, signed balances, overdrafts.

**Operate &amp; extend**

- [Operations](docs/09-operations.md) — verify, rebuild, Octane, batch posting, drift recovery, legacy imports.
- [Extension Points](docs/10-extensions.md) — the five ways to extend the package.
- [Events &amp; Exceptions](docs/11-events-and-exceptions.md) — every event and exception, plus listener guidance.
- [Anti-features](docs/12-anti-features.md) — what the package will never do.
- [Testing](docs/13-testing.md) — how to test a system built on the package.
- [FAQ](docs/14-faq.md) — quick answers to common questions.

Testing
-------

[](#testing)

```
composer test
```

The full suite runs against SQLite by default. To exercise the real-database CHECK constraints locally:

```
DB_DRIVER=pgsql DB_HOST=127.0.0.1 DB_DATABASE=ledger_test DB_USERNAME=postgres composer test
DB_DRIVER=mysql DB_HOST=127.0.0.1 DB_DATABASE=ledger_test DB_USERNAME=root  composer test
```

Changelog
---------

[](#changelog)

See [CHANGELOG](CHANGELOG.md). Upgrading from `0.9.x` is covered in [UPGRADING.md](UPGRADING.md) — most installs need only `php artisan migrate`.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details — including the explicit list of changes that will be rejected, because this package's invariants are the reason it exists.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Syriable](https://github.com/syriable)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance96

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 72.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

16d ago

Major Versions

v0.9.1 → v1.0.0-rc.12026-05-24

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/286110444?v=4)[syriable](/maintainers/syriable)[@syriable](https://github.com/syriable)

---

Top Contributors

[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (34 commits)")[![alkhatibsy](https://avatars.githubusercontent.com/u/23545455?v=4)](https://github.com/alkhatibsy "alkhatibsy (7 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (5 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravelfinanceimmutableAccountingdouble entryledgersyriable

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/syriable-laravel-ledger/health.svg)

```
[![Health](https://phpackages.com/badges/syriable-laravel-ledger/health.svg)](https://phpackages.com/packages/syriable-laravel-ledger)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45344.0k1](/packages/pressbooks-pressbooks)[flarum/core

Delightfully simple forum software.

261.4M2.2k](/packages/flarum-core)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
