altek/accountant
Altek Accountant adds double-entry bookkeeping to Laravel: define a chart of accounts, record debit and credit journal entries, and query balances per account. It ships with migrations, Eloquent models, and helpers so you can build ledgers directly in your app without an external accounting service.
Accountant wires Eloquent's model events into a persistence layer made of ledgers (who/where the change happened) and journals (the ordered set of model changes produced in that context). Install and bootstrap it in three steps:
composer require altek/accountant
php artisan vendor:publish --provider="Altek\Accountant\AccountantServiceProvider"
php artisan migrate
The publish step drops config/accountant.php and the package migrations. Read the config first — it drives which events are recorded, how the acting user is resolved into a Ledger, and what columns are captured per record. Then attach the trait to the model you want audited:
use Altek\Accountant\Contracts\Recordable;
use Altek\Accountant\Recordable as RecordableTrait;
class Article extends Model implements Recordable
{
use RecordableTrait;
protected $recordableEvents = ['created', 'updated', 'deleted', 'restored'];
}
First useful case: an Article update. Visit the record, edit the title, and query $article->journals — you should see one journal containing a record whose properties are the pre-change values. If that returns nothing, the event list, the trait, or the ledger resolution is wrong, in that order.
Automatic CRUD auditing. Declare only the events you care about in $recordableEvents. A common pattern is a base model with the trait and a default event list, then override per model where e.g. restored should not apply. Pair it with a created/updated/deleted trio so your history is gapless.
Manual entries for domain events. Not every change is a model write. Use the Accountant facade to record a label alongside a journal, so domain actions (payments, approvals, exports) sit in the same timeline as CRUD:
use Altek\Accountant\Facades\Accountant;
Accountant::record('order.approved', ['order_id' => $order->id]);
Group related writes by recording them under a single journal when they represent one atomic user action.
Ledger as a request/session scope. Ledger ties a journal to the acting principal. Customize resolution when you have multi-guard auth or service-level actors:
Accountant::resolveLedgerUsing(fn () => TenantUser::find(Tenant::current()->actorId));
For CLI and queued work, set the ledger explicitly before the batch and reset after — otherwise you get orphan journals with a null ledger id.
Per-record history views. The journals relation is queryable; scope it by event type and eager-load the user through the ledger for audit screens:
$history = $article->journals()
->with('ledger')
->latest()
->paginate(25);
Compose with soft deletes and revisions. Because account records hold the old property values, they double as a lightweight revision store. A pattern is to build a "diff" presenter from consecutive journals and only rehydrate the full model when the user actually clicks "revert".
Revert workflow. Reverting is a first-class concept in the package but should be wrapped by you: gate it behind authorization, log the revert itself as a journal entry, and never let it run inside a transaction that also mutates unrelated models.
composer.json, and smoke-test trait boot on a fresh install before adopting it. If you're on Laravel 8+, benchmark against owen-it/laravel-auditing or spatie/laravel-activitylog and treat Accountant as a migration source rather than a target.$recordableEvents = [] records nothing and throws no error. When auditing "doesn't work", verify the property is present and spelled correctly before touching config.config/accountant.php for the column type toggle before migrating.Auth::user() by default. Bind a ledger manually or make your ledger resolver fall back to a system principal, otherwise journals are unattributed.Recordable adds relations and helpers; if a model already defines journals() or a conflicting boot hook, you'll get obscure failures. Rename your relation or move the trait to a base class.updated produces an unbounded journal chain. Keep such writes out of $recordableEvents or guard with a static "recording" flag.Model::where(...)->update() and insert() don't fire Eloquent events, so no journals exist. For bulk changes, wrap the operation and call Accountant::record() with a summary payload, or chunk through save().DB::listen output around the model save.Ledger or Journal models via container bindings and the config's model map, and use resolveLedgerUsing for principal resolution. Custom Accountant implementations let you change how journals are grouped without forking the package — prefer this over editing vendor code, which you'll need to reapply on every update.How can I help you explore Laravel packages today?