mukadi/core-wallet-manager
Core wallet management for Laravel apps: define wallets, track balances and transactions, and build on a reusable ledger foundation for credits, debits, and account histories. A lightweight package for payment, points, or credit systems.
composer require mukadi/core-wallet-manager
Before writing a line of application code, spend 30 minutes reading the package rather than the README. With 2 stars and 0 dependents, there is no tutorial corpus, no Stack Overflow history, and no blog posts to fall back on — src/ and tests/ are the only real documentation.
Order of inspection:
composer.json — PHP constraint, required extensions (does it need bcmath/gmp?), and whether there is an extra.laravel.providers block. A "lightweight PHP library" is usually framework-agnostic, so expect no service provider and no auto-discovery.src/ — usually a WalletManager-style facade or a static factory. This is your public API surface; everything else is internals.Contracts/ (if present) — the interfaces tell you what is designed to be swapped and what is an extension point.tests/ — the fastest way to see a working credit → debit → transfer cycle. Copy the setup block into a scratch test.First use case to build (in a throwaway branch): a single-currency wallet for a User, credit it 100, debit it 30, read back the balance, and list transactions. That one loop exercises persistence, the manager, rounding, and the model accessors — and will surface 90% of the rough edges before you design anything on top.
Wiring into Laravel, since it likely won't be automatic:
// app/Providers/AppServiceProvider.php
$this->app->singleton(WalletManager::class, fn () => new WalletManager(/* config */));
Then copy (don't --publish if there's no publish tag) the migrations into database/migrations, and keep them in your repo under version control so a composer update can't silently change your schema.
Wrap, don't scatter. Never call package classes from controllers or models. Create one App\Services\WalletService (or two: WalletService + TransferService) that owns the manager, the transaction boundary, and the idempotency logic. This is non-negotiable for a package this small — the wrapper is your fork-point when you inevitably need to patch behavior.
final class WalletService
{
public function __construct(private WalletManager $manager) {}
public function credit(Wallet $wallet, int $amountMinor, string $idempotencyKey): Transaction
{
return DB::transaction(function () use ($wallet, $amountMinor, $idempotencyKey) {
$wallet = Wallet::whereKey($wallet->getKey())->lockForUpdate()->firstOrFail();
// idempotency check, then delegate to the manager
});
}
}
Money is integers. Store minor units (int), format only at the edge (Blade/API resource). If the package takes floats or strings, normalize at the boundary of your WalletService and never let a float reach the library. Verify how it rounds — half-up vs. truncation is a support ticket waiting to happen.
Append-only ledger, derived balance. Treat transaction rows as immutable facts. If the package exposes an update/delete on transactions, don't use it; model corrections as reversal entries. Maintain a cached balance column only if the package does, and add a nightly reconciliation command that recomputes from the ledger and alerts on drift.
Concurrency is your problem, not the library's. Wrap every mutation in DB::transaction and take a lockForUpdate() on the wallet row before reading the balance. A "lightweight" library very often reads-then-writes without locking; if the source confirms that, the lock in your wrapper is what prevents double-spend. Add a unique index on (wallet_id, idempotency_key) at the DB level so retries from a queued job are safe.
Transfers span two wallets. Lock in a deterministic order (lowest id first) to avoid deadlocks, and require both legs to be in one DB transaction. If the package's transfer helper doesn't do this, write it yourself against the credit/debit primitives.
Integrate through events. After a successful mutation, dispatch your own domain event (WalletCredited, WalletDebited) from the wrapper — not from inside package callbacks. Everything downstream (notifications, accounting export, fraud checks) subscribes to your event, so you stay decoupled from the package's event naming.
Test against the real DB. If the package uses row locking or driver-specific SQL, in-memory SQLite will either fail or silently pass a race you actually have. Run the wallet test suite against the same engine as production (MySQL/Postgres) in CI.
"mukadi/core-wallet-manager": "1.0.3", not ^1.0), commit your composer.lock, and read every release diff. Assume you may need to fork; your WalletService wrapper is what makes that cheap.extra.laravel first, then bind manually.publishes() calls; if absent, copy the migration files by hand and adjust table names/timestamps to avoid collisions with existing tables.sum(credits) - sum(debits) on every read (fine, but O(n) and slow past ~10k rows) and some keep a denormalized column without a lock (fast, but wrong under concurrency). Know which you have before you scale.false, or allow a negative balance? Wrap it and normalize to a single InsufficientFundsException so callers don't branch on library internals.credit() call and read the exact SQL. That reveals the locking behavior, the rounding, and whether there's a hidden SELECT per write. Log every ledger mutation with the idempotency key and the resulting transaction id at info level — with no community tooling, your logs are the only forensic trail.Contracts (storage driver, transaction repository) over subclassing concrete classes. If everything is final or concrete, keep the wrapper thin and put behavior in your own layer instead of forking immediately.$this->authorize('debit', $wallet)); don't assume the package enforces ownership of the wallet.WalletService interface free of package types in its signatures (accept your own DTOs and IDs, return your own TransactionResult). If you ever swap to bavix/laravel-wallet or a hand-rolled ledger, only the wrapper's internals change.How can I help you explore Laravel packages today?