laravel-wallet splits purchases into limited and unlimited products, and warns you off one of them
It's easy to work with a virtual wallet
At a glance
- What is it?
- bavix/laravel-wallet gives an Eloquent model a balance, deposits, withdrawals and a purchase flow in two trait/interface pairs. From version 9 it distinguishes an unlimited product from a limited one, and the README explicitly recommends against the limited interface in a shopping cart because it costs you one query per product.
- Who is it for?
- laravel-wallet fits a Laravel application that needs per-user balances, purchases and refunds without building a ledger from scratch, and its transaction-level guarantees matter more to you than a hand-rolled balance column. Do not reach for it to store money in the regulatory sense, because a balance integer on your user table is an accounting convenience rather than a payment processor or a double-entry ledger.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- bavix/laravel-wallet1,336 ★ · +0
| Repository | 2026-10-17 | 2026-11-07 |
|---|---|---|
| bavix/laravel-wallet | 1.3k | 1.3k |
0 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A trait and an interface, then four methods
The whole basic surface fits in a few lines, which is the clearest sign of what the package is.
You add the `HasWallet` trait and the `Wallet` interface to a model:
use Bavix\Wallet\Traits\HasWallet;
use Bavix\Wallet\Interfaces\Wallet;
class User extends Model implements Wallet
{
use HasWallet;
}Then the model has a balance and four operations:
$user = User::first();
$user->balanceInt; // 0
$user->deposit(10);
$user->balance; // 10
$user->balanceInt; // int(10)
$user->withdraw(1);
$user->balance; // 9
$user->forceWithdraw(200, ['description' => 'payment of taxes']);
$user->balance; // -191Two details in that example are the API's design rather than an accident. There is a `balance` and a `balanceInt`, so one returns a formatted value and the other an integer suitable for arithmetic, which is the difference between a display value and an amount. And `forceWithdraw` moves the balance negative, which is named `force` because the ordinary `withdraw` refuses to go below zero. That is the escape hatch for a debt or a correction, and the metadata argument is how you record why.
The second pair of traits is for the other side of the transaction. `CanPay` and the `Customer` interface mark a model as something that can pay, and the package then treats any other `HasWallet` model as a product.
Eager loading has its own distinction, with `User::with('wallet')` for a single wallet and `User::with('wallets')` for the multi-wallet functionality, which is the case where one model holds several balances.
Two product interfaces, and the author tells you which one to avoid
From version 9 there are two product interfaces, and the distinction is about how many of a thing a customer may have rather than about payment.
`ProductInterface` is for an unlimited number of products. The implementation needs `getAmountProduct`, which returns an int or a string, and `getMetaProduct`, which returns an array or null. In the example, the amount is 100 and the metadata carries a title and a description built from the model, so the purchase record has something readable in it rather than only an identifier.
`ProductLimitedInterface` is for a limited number, and adds `canBuy`, which receives the customer, a quantity and a force flag, and returns a bool. The commented example inside it is the interesting part: it shows the constraint logic as a call to `PurchaseQueryHandlerInterface` with `PurchaseQuery::create`, checking whether the customer has already bought this product. The interface therefore expects you to query prior purchases to answer whether another is allowed.
And then the recommendation, stated plainly in the README: do not use the limited interface when working with a shopping cart. If you are working with a shopping cart, you should override the `PurchaseServiceInterface` instead, and with it you can check the availability of all products with one request, with no N-queries in the database.
That is the most useful opinion in the README. Using the limited interface for a cart can require per-item availability checks, producing N-queries in the database. The fix is not that the queries are slow in isolation, it is that the shape of the interface forces per-item checks where the operation is inherently batch.
So the choice is between a per-item constraint hook that composes badly, and overriding a service to implement your own batching. For a single purchase flow the limited interface is fine; for a cart it is the wrong tool.
pay throws, safePay returns, refund reverses
The purchase flow makes its error policy visible through the method names.
$user = User::first();
$user->balance; // 100
$item = Item::first();
$user->pay($item); // If you do not have enough money, throw an exception
var_dump($user->balance); // 0
if ($user->safePay($item)) {
// try to buy again
}
var_dump((bool) app(PurchaseQueryHandlerInterface::class)->one(PurchaseQuery::create($user, $item))); // bool(true)
var_dump($user->refund($item)); // bool(true)
var_dump((bool) app(PurchaseQueryHandlerInterface::class)->one(PurchaseQuery::create($user, $item))); // bool(false)`pay` throws if there is not enough money, and the comment in the code states it. `safePay` returns a bool, which the example uses to branch rather than to catch. For a purchase button the difference is not cosmetic: a thrown exception that reaches a controller produces a 500 or an error page, while a false gives you a message.
The last four lines are the part worth reading twice. After paying, the purchase query returns true, meaning the customer now owns the product. After `refund`, it returns false. That is the package's receipt, and it means refund is not simply an inverse balance operation: it is recorded as a state you can query, which is what lets a limited product become limited again after a return.
For fractional amounts there is a parallel path: the `HasWalletFloat` trait and the `WalletFloat` interface. The name says what it adds and nothing more, and the fact that it is a separate trait rather than a flag on `HasWallet` means float balances are opt-in rather than implicit.
The support policy shows the 11.x window closing
The support policy table is short and its dates are the most actionable thing in the README.
Version 12.x requires Laravel `^13.0` on PHP 8.3, 8.4 or 8.5, released on Apr 18, 2026, with improvements ending May 1, 2028 and support ending Sep 6, 2028. Version 11.x requires Laravel `^11.0`, `^12.0` or `^13.0` on the same PHP versions, released Mar 14, 2024, with improvements ending May 1, 2026 and support ending Sep 6, 2026.
Read the 11.x row carefully if you are on it. Improvements ended on May 1, 2026 and full support ends on Sep 6, 2026, both of which are in the past relative to today. So a project on 11.x is past its improvement window and inside the final months of its support window, and 12.x requires Laravel 13.
That creates a real constraint for a team still on Laravel 11 or 12: their only path is 11.x, whose clock is running out, and moving to 12.x means moving to Laravel 13 at the same time.
The release history shows how active this is. Recent releases include 12.1.1 on 2026-09-29, 11.5.3 on 2026-09-29 and 12.1.0 on 2026-08-27, so both lines are receiving patches concurrently. A patch to 11.x after its improvement window has ended is consistent with the security and support dates rather than a contradiction of them.
An upgrade guide is published separately, and the README points to it for the migration rather than describing it here.
What the toolchain says about how the package is maintained
The repository root is a list of quality tooling, and for a package that moves money around a customer's balance that list is the point.
There are four PHPStan configurations, `phpstan.common.neon` plus separate ones for src and tests, each with its own baseline file. Baselines mean the existing violations are recorded rather than silently tolerated, so a new violation fails the build.
`deptrac.yaml` enforces architectural dependency rules, which is the mechanism that stops a value object from reaching into an Eloquent model and is exactly what you want in a package with this many interfaces.
`infection.json.dist` configures mutation testing, and the README carries a Stryker mutation testing badge. Mutation testing measures whether your tests actually detect wrong behaviour, which for a ledger package is the difference between coverage and evidence.
Then there is `ecs.php` for coding standards, `rector.php` for automated refactoring rules, `phpunit.xml` for tests, `coverage` through Coveralls with a badge, `.phpstorm.meta.php` for IDE type resolution, `.phpactor.json` for the PHPactor editor, and `.whitesource` for a dependency vulnerability service.
The documentation is built with VitePress from the `docs/` directory, driven by a `package.json` whose only dependency is VitePress, and the author is named as Maxim Babichev.
Three extensions are listed as separate repositories: Swap for exchange rates, uuid for UUID model keys, and Warm Up for refreshing wallet balances. That last one is worth noting because it implies balance computation can be non-trivial at scale, which is a design consequence of doing more than storing an integer.
Editorial conclusion
laravel-wallet fits a Laravel application that needs per-user balances, purchases and refunds without building a ledger from scratch, and its transaction-level guarantees matter more to you than a hand-rolled balance column. Do not reach for it to store money in the regulatory sense, because a balance integer on your user table is an accounting convenience rather than a payment processor or a double-entry ledger. Verify first which major line your Laravel version maps to, because the support policy shows 11.x covering Laravel 11 through 13 with improvements ending in May 2026 while 12.x targets Laravel 13 alone.
Frequently asked questions
How do I add a wallet to a Laravel model?
Add the `HasWallet` trait and implement the `Wallet` interface on the model. The model then exposes `balance`, `balanceInt`, `deposit`, `withdraw` and `forceWithdraw`, and you load the balance with `with('wallet')` or `with('wallets')` for multi-wallet setups.
Which Laravel and PHP versions does laravel-wallet support?
Version 12.x requires Laravel ^13.0 on PHP 8.3, 8.4 or 8.5, with improvements ending May 1, 2028 and support ending Sep 6, 2028. Version 11.x covers Laravel ^11.0, ^12.0 and ^13.0 on the same PHP versions, with improvements ending May 1, 2026 and support ending Sep 6, 2026.
What is the difference between ProductInterface and ProductLimitedInterface?
ProductInterface is for an unlimited number of products and needs only `getAmountProduct` and `getMetaProduct`. ProductLimitedInterface adds `canBuy`, which is expected to query prior purchases through `PurchaseQueryHandlerInterface`.
Why does the README say not to use the limited interface in a shopping cart?
Because `canBuy` runs per item, so a cart issues one availability query per product line and the README describes N-queries in the database. For a cart it recommends overriding `PurchaseServiceInterface` so all products are checked with one request.
What happens when a customer does not have enough balance to pay?
`pay` throws an exception when the balance is insufficient, while `safePay` returns a bool you can branch on. After a refund, a purchase query for that product returns false again, so the receipt is recorded rather than only the balance adjusted.
How do I handle fractional balances in laravel-wallet?
With a separate pair: the `HasWalletFloat` trait and the `WalletFloat` interface, rather than a flag on the integer wallet. That keeps float handling opt-in instead of implicit.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bavix-laravel-wallet)Star history chart for your README
[](https://hysenlabs.com/projects/bavix-laravel-wallet)Was this useful?