bavix/laravel-wallet
Virtual wallet system for Laravel: attach wallets to models, track balances, perform deposits/withdrawals/transfers, handle atomic transactions and events, and support multi-currency and bookkeeping. Includes docs, benchmarks, and upgrade guide.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require bavix/laravel-wallet
php artisan vendor:publish --tag=laravel-wallet-config
Publish the config file to customize settings like lock (race condition handling) and cache (state storage).
Basic Model Integration:
Add traits and interfaces to your model (e.g., User):
use Bavix\Wallet\Traits\HasWallet;
use Bavix\Wallet\Interfaces\Wallet;
class User extends Model implements Wallet
{
use HasWallet;
}
This adds balance, balanceInt, deposit(), withdraw(), and other wallet methods.
First Transaction:
$user = User::first();
$user->deposit(100); // Deposit 100 units
$user->withdraw(20); // Withdraw 20 units
Verify balance:
$user->balance; // Returns float (e.g., 80.00)
$user->balanceInt; // Returns integer (e.g., 80)
Eager Loading: Optimize queries with:
User::with('wallet')->get(); // For single wallet
User::with('wallets')->get(); // For multi-wallet
Standard Transactions:
deposit()/withdraw() for basic operations.$user->refund($product); // Reverts a purchase
Purchases with CanPay:
User with CanPay trait and Customer interface:
use Bavix\Wallet\Traits\CanPay;
use Bavix\Wallet\Interfaces\Customer;
class User extends Model implements Customer
{
use CanPay, HasWallet;
}
Item) with ProductInterface or ProductLimitedInterface:
class Item implements ProductInterface
{
use HasWallet;
public function getAmountProduct(Customer $customer): int|string
{
return 100;
}
}
$user->pay($item); // Throws exception if insufficient funds
$user->safePay($item); // Returns bool (false if failed)
Fractional Currency Support:
HasWalletFloat for decimal balances:
class User implements Wallet, WalletFloat
{
use HasWalletFloat;
}
$user->depositFloat(1.99); // Deposit fractional amount
$user->balanceFloat; // Returns 1.99
Multi-Wallet Systems:
User):
$user->wallets()->attach($walletId, ['type' => 'premium']);
wallets() relationship.Atomic Operations:
AtomicServiceInterface for race-condition-safe operations:
app(AtomicServiceInterface::class)->block($wallet, function () {
$wallet->withdraw(100);
$user->update(['premium_until' => now()->addDays(30)]);
});
app(AtomicServiceInterface::class)->blocks([$wallet1, $wallet2], function () {
$wallet1->withdraw(50);
$wallet2->withdraw(50);
});
Transactions:
DB::transaction(function () use ($wallet) {
$wallet->withdraw(100); // Locked until commit/rollback
// Other DB operations...
});
Race Conditions:
wallet.php for lock/cache drivers (e.g., Redis for production):
'lock' => ['driver' => 'redis', 'seconds' => 1],
'cache' => ['driver' => 'redis'],
Custom Logic:
canBuy() for ProductLimitedInterface to enforce rules:
public function canBuy(Customer $customer, int $quantity = 1): bool
{
return $customer->hasRole('premium') || $quantity <= 3;
}
Extensions:
laravel-wallet-swap for exchange rates or laravel-wallet-uuid for UUID support.Events:
wallet.deposited, wallet.withdrawn) via Laravel’s event system.Wallet Locking:
DB::transaction inside another).Race Conditions:
WalletIsLockedException.lock.driver = 'array' (default) before switching to Redis.Fractional Precision:
balanceFloat uses PHP’s float type, which may introduce rounding errors. For financial apps, consider:
balanceCents = 199 for $1.99).bcmath for precise calculations.Multi-Wallet Quirks:
default pivot value. Ensure consistency when attaching wallets:
$user->wallets()->attach($walletId, ['default' => true]);
Atomic Service Limits:
blocks()) creates N lock requests, which can be slow. Use sparingly.Lock Issues:
wallet.log (if configured) for blocked wallets:
'log' => [
'enabled' => true,
'path' => storage_path('logs/wallet.log'),
],
redis-cli DEL "wallet:lock:{wallet_id}"
Balance Mismatches:
balance vs. balanceInt for fractional values. Use balanceFloat for debugging:
dd($user->balance, $user->balanceInt, $user->balanceFloat);
Performance:
Cart:Pay benchmarks. If slow, check:
with('wallet')).wallets table (user_id, currency).array).Event Debugging:
AppServiceProvider:
public function boot()
{
Wallet::deposited(function ($wallet, $amount) {
Log::info("Deposited {$amount} to wallet {$wallet->id}");
});
}
Custom Wallets:
Bavix\Wallet\Models\Wallet for custom logic (e.g., PremiumWallet):
class PremiumWallet extends Wallet
{
public function premiumWithdraw($amount)
{
if ($this->user->hasRole('premium')) {
return $this->withdraw($amount);
}
throw new \RuntimeException('Premium required');
}
}
Dynamic Pricing:
getAmountProduct() to apply discounts:
public function getAmountProduct(Customer $customer): int
{
return $this->price * (1 - ($customer->discount / 100));
}
Webhooks:
event(new WalletDeposited($wallet, $amount));
WalletDeposited::listen(function ($event) {
// Notify Stripe/PayPal
});
Testing:
WalletTestCase (if provided) or mock the Wallet facade:
$this->partialMock(Wallet::class, ['withdraw'])
->expects($this->once())
->method('withdraw')
->with(100);
Fallback Logic:
WalletIsLockedException gracefullyHow can I help you explore Laravel packages today?