Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ledger Laravel Package

abivia/ledger

Abivia Ledger is a Laravel package for double-entry financial record keeping, providing an Eloquent-friendly foundation for organizing accounts and recording ledger transactions. Use it to add accounting-style tracking and balance management to PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and its required bcmath extension, then publish the migrations:

composer require abivia/ledger
php artisan vendor:publish --provider="Abivia\Ledger\LedgerServiceProvider"
php artisan migrate

Open config/ledger.php first — that is where you set the database connection, table prefix, and any multi-domain behavior. Skim the ledger models under Abivia\Ledger\Models (LedgerAccount, LedgerJournal, LedgerEntry, LedgerTransaction, LedgerRoot) so you understand that accounts and journals are entities referenced by short human codes, not IDs.

Every entry point to the package goes through the Ledger facade. The canonical "hello world" is to create a root ledger, a chart of accounts, a journal, and a balanced transaction:

use Abivia\Ledger\Facades\Ledger;

$root = Ledger::createRoot([
    'name'     => 'Acme Corp',
    'currency' => 'CAD',
]);

Ledger::addAccount(['code' => '1000', 'name' => 'Cash',              'type' => 'asset']);
Ledger::addAccount(['code' => '4000', 'name' => 'Sales Revenue',     'type' => 'revenue']);

Ledger::addJournal(['code' => 'GJ', 'name' => 'General Journal']);

$txn = Ledger::addTransaction([
    'journal'     => 'GJ',
    'date'        => now()->toDateString(),
    'description' => 'Cash sale',
]);

Ledger::addEntry($txn, ['code' => '1000', 'debit'  => '100.00']);
Ledger::addEntry($txn, ['code' => '4000', 'credit' => '100.00']);

$txn->post();

That is the first use case worth wiring up on day one — a tiny seed script or artisan command that stands up a chart of accounts and posts one balanced transaction. Everything else (subledgers, reports, reversals) builds on that shape.

Implementation Patterns

Wrap the facade in your own service. The facade is global and assumes a single active root/domain at a time. For anything non-trivial, create app/Accounting/LedgerService.php around it and inject that. It keeps controllers thin and gives you a single place to centralize the setDomain() / setRoot() behavior for multi-tenant installs.

Treat accounts and journals as seed data. Put the chart of accounts and journal definitions into a seeder or migration rather than creating them at runtime. Lookup is by code, so downstream code should reference '1000', not an auto-increment ID. This lets you keep business logic decoupled from database rows.

Build transactions entry-by-entry, then validate. The pattern that scales is: create the transaction header, append entries with addEntry, then call post(). Between the adds you can keep the transaction in a "raw" state and only promote it when a balance check passes. Do not sprinkle post() calls inside loops.

Model subledger references on the entry code. For A/R and A/P, abivia/ledger supports an extended code on the entry (for example a customer code appended to the control account). Keep that convention in one constant so your reconciliation queries match your posting code exactly.

Query balances instead of summing entries yourself. The package exposes balance lookups keyed by account code and date range. Use those for trial balances, aging schedules, and dashboards; do not roll your own SUM(debit - credit) against ledger_entries unless you also honor the package's domain and journal filters.

Pair every posted correction with a reversing transaction. Posted entries are effectively immutable. When a user "edits" an invoice, write a reversing transaction against the original journal and post a fresh one. This is a pattern, not a hard requirement, but doing it any other way will produce a ledger that cannot be audited.

Integrate through jobs, not requests. Posting a transaction touches multiple tables and runs balance validation. Dispatch a PostJournalEntry job rather than posting inside an HTTP handler, especially for imports, invoice finalization, and payment reconciliation.

Gotchas and Tips

  • Amounts are strings, not floats. The package relies on bcmath, and passing a PHP float will produce rounding drift you will only notice at period close. Normalize every amount with number_format($v, 2, '.', '') (or a bcadd) before handing it to addEntry.
  • Unbalanced transactions fail at post(), not at addEntry. If you write a helper that adds a debit and forgets the matching credit, the error surfaces late. Assert the debit total equals the credit total in your wrapper before calling post(), and log the offending transaction payload when the assertion fails.
  • Account code is a hierarchy delimiter. Codes such as 1000.01 nest under 1000. If you are generating codes programmatically, confirm the separator matches the package's expectation, otherwise balance() on the parent will silently return zero.
  • "Posted" is a one-way door in practice. There is no delete that maintains integrity. If you must correct something, reverse and repost, and if you are debugging in dev, run migrate:fresh --seed rather than trying to unpost by hand.
  • The active root/domain is global state. The facade remembers whichever ledger context was last set. Long-running queue workers will leak that between jobs. Reset the domain at the top of every job that touches the ledger, and never rely on the previous job's context.
  • Date handling is your responsibility. addTransaction accepts an explicit date but does not interpret fiscal periods. If you need "post into period 2024-03 only", enforce it in your service layer — the package will happily accept a backdated transaction and shift every historic balance report you have.
  • Currency is a root-level attribute, not per-entry. You cannot mix currencies inside one root without extra tables on your side. Multi-currency means one root per currency (or a conversion layer you own), and that decision is hard to reverse once journals exist.
  • Debug with the raw models. When post() throws, dump the LedgerTransaction and its LedgerEntry relations through Eloquent; the facade's exception messages are terse. The state field on the transaction tells you how far it got.
  • Migration conflicts on large apps. The published migrations create tables with a configurable prefix — set that before the first migrate on a shared database, because renaming later means hand-editing schema.
  • Version-pin the package. abivia/ledger has had breaking changes in minor releases; add a caret constraint matched to the version you develop against and re-read the changelog before upgrading.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource