Download the PHP package alifaraun/wallet without Composer

On this page you can find all versions of the php package alifaraun/wallet. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package wallet

Laravel Wallet

Latest Version on Packagist Total Downloads

A multi-currency, limit-aware wallet & ledger package for Laravel — built for correctness under concurrency and for high-traffic scenarios like voucher/reseller balances.

It stores money as integer minor units, guards every balance change with a pessimistic row lock inside a transaction, supports per-(holder, currency) wallets, direction-aware transaction limits plus daily maximums, per-wallet overdraft, and emits events instead of forcing its own notifications.


Table of contents


Why this package

Concern How it's handled
Float rounding on money Stored as signed BIGINT minor units; exposed to you as ordinary numbers via a cast. Input conversion still rounds at the money boundary, but stored balances and ledger math stay integer-only.
Concurrent balance writes SELECT … FOR UPDATE inside DB::transaction; both credits and debits are serialized per wallet.
Multiple currencies One wallet row per (holder, currency); the holder is polymorphic (any model).
Spend/receive limits DB-backed override hierarchy for debits; flat per-currency config for credits; per-transaction limits plus daily maximums.
Overdraft / credit lines Per-wallet min_balance floor (default 0).
Daily-limit cost at scale O(1) running counter table (pluggable; a SUM() fallback ships too).
App coupling Holder is any model; the causer is an explicit parameter (never reads auth()); notifications are your event listeners.

Requirements

On Laravel 10? Install the 1.0 release, which targets Laravel 10 / PHP 8.1+: composer require alifaraun/wallet:^1.0.


Installation

Core wallet tables auto-load from the package, so the fastest install path is:

If you prefer app-owned migration stubs, publish and run the core migrations:

Only if you switch to EloquentCurrencyRepository should you publish the optional currencies migration:

Optionally publish the config:

The service provider and the Wallet facade are auto-discovered.

Required: set a default currency before moving money. The package ships no fallback, so add WALLET_DEFAULT_CURRENCY=USD to your .env (or set default_currency in the published config/wallet.php) to a code present in your currencies list. Any call that omits a currency while this is unset throws MissingDefaultCurrencyException.


Quick start

1. Make a model a wallet holder

2. Create a wallet and move money

Amounts are ordinary major-unit numbers (19.99). type is any string (or a BackedEnum). causer is explicit and may be null — the package never reads auth() for you.

3. Fetch a holder's wallet later

walletOrCreate() / createWallet() are idempotent fetch-or-create: if a wallet for that (holder, currency) already exists it's returned as-is. The name, minBalance, extra, ref, lowBalanceThreshold and lowBalanceMode arguments apply only when a new wallet is created — they never update an existing one. name, min_balance, low_balance_threshold, low_balance_mode, is_active and extra can be changed afterwards directly on the wallet ($wallet->update([...])); ref, currency and balance are the wallet's immutable identity/ledger fields and cannot be mass-assigned.

Currency codes are case-insensitive here too ('usd' resolves the same wallet), and the currency is optional everywhere it's accepted — leave it out to use config('wallet.default_currency'). That config key has no default and must be set (via WALLET_DEFAULT_CURRENCY or directly in config/wallet.php); omitting a currency while it is unset throws MissingDefaultCurrencyException.


Core concepts


Operations

All operations are available on the Wallet facade or by injecting Alifaraun\Wallet\WalletManager.

credit — add funds

debit — remove funds

transfer — move funds between two wallets (same currency)

Both legs run in one transaction with both rows locked in a deadlock-safe order; if either leg fails, the whole transfer rolls back. Each leg is limit-checked — the sender's debit limits and the receiver's credit limits both apply. Different currencies throw CurrencyMismatchException, and transferring to the same wallet throws InvalidOperationException.

An optional fee is charged to the sender on top of the amount: the sender's wallet loses amount + fee while the receiver gets the full amount. The fee leaves the system (it is not credited anywhere) — use it to model a processor/service cut, or pass fee: 0 (the default) for a 1:1 move.

Pass an optional reference to make the transfer idempotent. It keys the sender's debit leg (and the receiver's credit leg), so a replayed transfer with the same reference hits the per-wallet uniqueness guard on the sender and rolls the whole transfer back with DuplicateReferenceException instead of moving money twice. Both legs share the reference, so it also serves as a lookup key for the pair.

refund — give money back for something that happened

A refund is a credit tagged refund; it is limit-checked like any credit. It can be partial and does not erase the original transaction.

reverse — undo a transaction exactly

A reversal applies the exact opposite of the original's net amount and links back to it (so the original drops out of the notReversed() scope). Because it's a correction, it bypasses limits, the overdraft floor, and the active-wallet check — undoing something must always succeed, so you can still reverse a movement after a wallet has been deactivated (is_active = false). A transaction can be reversed at most once: it uses a deterministic per-wallet reference (rev_<id>), so reversing it again — regardless of the causer — throws DuplicateReferenceException instead of crediting back twice, and a retry job and a manual admin action can't double-undo it.

reverse() refuses transfer legs. A transfer is two linked rows — a transfer_out on the sender and a transfer_in on the receiver — committed atomically by transfer() (both or neither). reverse() only ever touches one wallet, so reversing a single leg would unbalance the pair; it throws InvalidOperationException instead. To undo a whole transfer, move the funds back with a compensating transfer() in the opposite direction — you control the reference, description, and causer on that correction.

Reversing a credit whose funds were already spent can drive the balance below its floor — possibly negative. Because reverse() bypasses the min_balance floor by design, undoing a credit after that money already left the wallet (e.g. it was debited elsewhere) leaves the balance below min_balance. That is intentional — the funds are genuinely owed back — but it means a reversal can park a wallet under its floor without an InsufficientBalanceException. Reconcile such wallets deliberately rather than assuming a balance can never sit below its floor.

A reversal is a correction, not volume, so it does not count toward daily usage in either direction — it neither adds to the opposite direction's total nor decrements the original's. (Both trackers agree: CounterTableUsage records nothing for a reversal, and LedgerSumUsage excludes reversal rows.) This mirrors the fact that reverse() bypasses the limits themselves.


Money & currencies

Money is stored as integer minor units (cents, etc.) but you always work in major units:

You can pass amounts as int, float, or a numeric string — handy when the value arrives as text (a request field, a CSV cell). A string is validated like any amount (a non-numeric string throws InvalidAmountException, not a raw TypeError) and then converted to minor units the same way:

All three input types are rounded to the nearest minor unit. Stored balances, counters, and ledger math stay on integers; only the conversion boundary touches float space. To keep that boundary exact, every minor-unit value — each amount, fee, and the resulting balance — must stay within ±2⁵³ (Money::MAX_SAFE_MINOR, ~9×10¹⁵ minor units); anything beyond is rejected with InvalidAmountException (getReason() names whether it was the amount or the resulting balance) rather than stored imprecisely. For the same reason a currency may declare at most 15 decimals (Money::MAX_DECIMALS): an 18-decimal "wei"-style precision can't be represented exactly by this design and is refused the moment that currency is read. An amount that is positive but smaller than one minor unit (so it would round to 0, e.g. 0.004 USD) is also rejected rather than silently recorded as a no-op movement.

Currency precision comes from the configured currency source. The shipped default config('wallet.currencies') includes a small starter set:

Code Decimals Symbol
USD 2 $
EUR 2 €
GBP 2 £
LYD 3 ل.د
USDT 6 ₮

Codes are normalized to uppercase, so 'usd' and 'USD' resolve to the same wallet. Add/remove entries in the config array (any ISO 4217 code, with its decimal count), or swap the whole source (see Extending). There is intentionally no currency enum — the supported set is data you control, not code.


Limits

Limits are direction-aware and expressed as positive magnitudes. They are checked against the movement's net balance impact, which includes the fee: a debit of amount + fee is what the debit limits see, and a credit of amount - fee is what the credit limits see. So a fee can push a debit over its max bound, or pull a credit under its min bound.

Debit limits — DB-backed, with an override hierarchy

Stored in the wallet_limits table. Resolution is most-specific-first:

  1. a rule for the specific wallet, then
  2. a holder-type default (holder_type set, wallet_id null), then
  3. a global default (both null), then
  4. the config('wallet.limits.debit') fallback.

The first tier with any matching rule wins outright — tiers never blend.

List currency before the *_amount fields (as above). The amount columns are money-cast and read the row's currency to know the decimal precision, so a limit row whose amounts are assigned before its currency throws MissingCurrencyContextException.

Any bound left null (or with no matching rule at all) means unlimited — never a hard 0.

Each tier allows at most one rule per currency — active or not. Creating a second row for the same (wallet_id, holder_type, currency) scope throws DuplicateWalletLimitException, regardless of either row's is_active state. To change a rule, update the existing row (and toggle is_active on it) rather than creating another one alongside it.

Credit limits — config only

Credits are limited purely by config('wallet.limits.credit'), keyed by currency with a default fallback. There is no per-holder/per-wallet credit override.

A per-currency block is selected whole — it does not merge field-by-field with default. List every bound a currency entry cares about; any field it omits is treated as null (unlimited), not inherited from default.

Daily window

"Daily" means the calendar day in config('wallet.daily_window_timezone') (defaults to your app timezone), so limits reset at your midnight rather than the database server's.


Overdraft

Each wallet has a min_balance floor (default 0). A debit may take the balance down to — but not below — min_balance. Set it negative to allow an overdraft / credit line:


Low balance warning

Each wallet may set a low_balance_threshold (major-unit amount; null — the default — disables the feature entirely) and a low_balance_mode:

A wallet with no low_balance_mode of its own falls back to config('wallet.low_balance.default_mode') (default 'once').

Only balance-decreasing movements can fire it — debit, the transfer_out leg, and a reverse() that undoes a credit. A credit, refund, the transfer_in leg, and a reverse() that undoes a debit never fire it, even if the resulting balance is still at or below the threshold: those movements only ever move balance up. There is no extra "already warned" state to track — both modes are derived purely from the movement's own balance_before/balance_after (already recorded on every transaction) compared against the threshold, so concurrent movements on the same wallet are serialized by the same row lock that protects every other rule in the pipeline, and LowBalanceWarning is dispatched after commit like TransactionRecorded — see Events.


Reference uniqueness

Pass a reference to give credit/debit/refund/transfer a stable, deduplicated key. A reference is unique per wallet: reusing one on the same wallet is rejected with a DuplicateReferenceException instead of moving money twice (for transfer, the reference keys both legs, so a replay is rejected at the sender and the whole transfer rolls back):

The check is independent of the causer, so anyone reusing the reference (a retry job, then an admin) is rejected the same way. The same reference used on a different wallet moves money there independently. It's enforced both by an in-transaction pre-check (an existence-only query inside the wallet lock) and a unique database index (wallet_id, reference) as the race backstop. Catch DuplicateReferenceException to make a retry safe — its getReference() / getWallet() tell you what collided.

Every transaction stores a non-null reference: when you don't pass one, the package mints a globally unique ULID (e.g. txn_01j…), so an auto-generated reference is safe to use as a public identifier on its own. A caller-supplied reference is only unique per wallet — the same value (pay_123) can legitimately exist on other wallets — so look those up by (wallet_id, reference), not by reference alone (a bare lookup can match rows on several wallets and isn't separately indexed). Movements without a caller reference each get a distinct auto-reference, so they never collide.


Exceptions

All extend Alifaraun\Wallet\Exceptions\WalletException (which has a context(): array) and carry typed getters returning major-unit values.

Exception Thrown when Key getters
InsufficientBalanceException debit would breach the balance floor getWallet(), getAttemptedAmount(), getAvailableBalance(), getMinBalance()
TransactionLimitExceededException per-transaction limit breached getBreachedBound(), getLimit(), getAttemptedAmount()
DailyLimitExceededException daily window limit breached getBreachedBound(), getLimit(), getDailyTotal()
CurrencyMismatchException cross-currency transfer getFromCurrency(), getToCurrency()
WalletNotActiveException operating on an inactive wallet getWallet()
InvalidAmountException non-positive / non-finite / non-numeric amount, an amount (or resulting balance) beyond the safe integer range, or one that rounds to zero at the currency's precision getAttemptedAmount(), getReason()
InvalidAttributeException a string input (reference, type, description, name, ref, currency) exceeds its column length, or an extra bag exceeds its size cap getAttribute(), getMaxLength(), getValue()
InvalidOperationException an operation is semantically invalid (a same-wallet transfer, or reverse() on a transfer_out/transfer_in leg) context()
UnknownCurrencyException currency not known to the source getCurrencyCode()
DuplicateReferenceException a movement reuses a reference already present on that wallet getReference(), getWallet()
MissingDefaultCurrencyException a currency was omitted but config('wallet.default_currency') is not set —
MissingCurrencyContextException a money-cast attribute was read or written without the model's currency loaded context()
WalletNotFoundException walletOrFail() finds no wallet getHolder(), getCurrency()

For a credit limit breach, getLimit() is null (credit limits come from config, not a DB row).

These exceptions carry sensitive financial detail — their messages and context() include wallet refs/ids, attempted amounts, available balances, and floors. Log them server-side, but don't render the raw message or context() to end users (especially across tenants). Catch the typed exception and surface your own sanitized message.


Events

The package sends no notifications; it fires events you listen to. WalletCreated, TransactionRecorded and LowBalanceWarning are dispatched after commit, so listeners never act on a rolled-back movement.

Event When Payload
WalletCreated a wallet is created $wallet
TransactionRecorded a movement commits $transaction, $wallet
LowBalanceWarning a balance-decreasing movement commits leaving balance at/below low_balance_threshold $wallet, $transaction, $threshold, $balanceBefore, $balanceAfter, $mode
LimitBreached just before a limit exception throws $wallet, $exception

For heavy traffic, make such listeners ShouldQueue so the locked transaction stays short.


Configuration

config/wallet.php (publish with the wallet-config tag):


Extending

Every moving part is a contract resolved from the container — bind your own or point the config at a different class.

Currency source

Ship-default reads the config array. Switch to the DB-backed model, or your own:

If you switch to EloquentCurrencyRepository, publish the optional currencies migration first with php artisan vendor:publish --tag=wallet-currency-migrations.

Daily-usage tracker

Limit resolver & models

Point config('wallet.limit_resolver') at your own LimitResolver, or config('wallet.models.*') at a subclass to add columns/behaviour without forking.

Custom transaction types

Any string works as a type; you are not limited to the package's TransactionType enum:


Concurrency & high traffic

The bundled test suite runs on SQLite and verifies sequential logic; it does not (and cannot, on a single connection) prove the lock under real contention. Concurrency safety rests on the lockForUpdate-in-transaction design.

Octane/Swoole note: correctness (the balance lock) is enforced by the database transaction and is unaffected by the runtime. The separate immutability guard on transaction/usage models — which blocks accidental direct writes — uses a per-class static flag and is a developer guardrail, not a hard boundary; under Swoole coroutines (concurrent coroutines in one worker) treat it as such. The real integrity guarantees are the DB constraints and the locked transaction.


Database schema

Table Purpose
wallets one balance per (holder, currency); min_balance, low_balance_threshold/low_balance_mode (nullable), is_active, extra (JSON), unique (holder_type, holder_id, currency)
wallet_transactions immutable ledger; non-null reference (unique per-wallet key / public id); polymorphic operation + causer; unique (wallet_id, reference); wallet_id FK is ON DELETE RESTRICT
wallet_limits debit limit rules (scope + currency); per-transaction and daily maximums are enforced
wallet_daily_usage running per-(wallet, day, direction) totals for O(1) daily checks; wallet_id FK is ON DELETE RESTRICT
currencies optional; publish it with wallet-currency-migrations only when using EloquentCurrencyRepository

All money columns are signed BIGINT minor units.

Wallets with history can't be silently hard-deleted. The wallet_transactions and wallet_daily_usage foreign keys use ON DELETE RESTRICT (not CASCADE), because a DB-level cascade would bypass the model's immutability guard and wipe the ledger with no event and no trace. To retire a wallet, set is_active = false (new credit/debit/transfer/refund movements then throw WalletNotActiveException; reverse() still works so you can correct history on a retired wallet). A genuine hard-delete is a deliberate act: clear the wallet's history first.


Testing

Static analysis and style:


Troubleshooting

Symptom Cause Fix
UnknownCurrencyException on walletOrCreate() The currency code isn't in your source Add it to config('wallet.currencies') (or your currencies table / custom repository) with its decimals and symbol.
InvalidAmountException Amount is 0, negative, non-finite, non-numeric, beyond the safe integer range (±2⁵³), smaller than one minor unit so it rounds to zero, or the movement's resulting balance would exceed that range (also: a non-finite minBalance) Pass a positive, finite amount within range that is at least one minor unit at the currency's precision; keep amounts and balances within ±2⁵³ minor units; a fee may be 0 but not negative; minBalance may be negative but must be finite.
InvalidAttributeException A reference/type/description/name/ref/currency string is longer than its column Shorten the value (reference ≤ 64, type ≤ 50, currency ≤ 10, the rest ≤ 255), or widen the column and the matching WalletManager constant. The package validates up front so you get this instead of a DB error.
DuplicateReferenceException A reference already exists on that wallet (e.g. a replayed request) References are unique per wallet — use a fresh one per distinct movement, or treat the exception as "already processed" and look up the existing transaction by (wallet_id, reference).
InvalidOperationException Same-wallet transfer, or reverse() called on a transfer_out/transfer_in leg Use distinct wallets for transfers; undo a transfer with a compensating transfer() instead of reverse().
WalletNotActiveException is_active is false on the wallet (credit/debit/transfer/refund only — reverse still works) Re-activate it ($wallet->update(['is_active' => true])) or branch on is_active before moving money.
InsufficientBalanceException Debit would breach min_balance (default 0) Fund the wallet, lower the amount, or set a negative min_balance for an overdraft line.
TransactionLimitExceededException / DailyLimitExceededException A per-transaction or daily bound was hit Inspect getBreachedBound() / getLimit(); adjust the wallet_limits row (debit) or config('wallet.limits.credit') (credit).
DuplicateWalletLimitException A wallet_limits row already exists for the same (wallet_id, holder_type, currency) scope (active or not) Update the existing rule in place (toggle is_active on it) instead of creating a second row for the same scope.
MissingCurrencyContextException when reading money fields The query did not select the model's currency column Include currency when partially selecting money-cast models such as wallets, transactions, or limit rows.
Balance looks wrong by a factor of 10/100 A wrong decimals for the currency Minor units depend on decimals; correct the currency definition (changing it after data exists needs a migration of stored values).
RuntimeException: "decimals" must be … between 0 and 15 A currency declares more than 15 decimals (e.g. 18 for wei) This float-boundary design can't represent that precision exactly; cap the currency at ≤ 15 decimals, or model the asset in a coarser unit.
Listeners didn't fire on a rolled-back movement By design WalletCreated / TransactionRecorded are afterCommit; only LimitBreached fires before the rollback.

Changelog

See CHANGELOG.md.

Contributing

Pull requests are welcome. Please run vendor/bin/phpunit, vendor/bin/phpstan analyse, and vendor/bin/pint before submitting.

Security

Hardening notes

If you discover a security issue, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). See LICENSE.


All versions of wallet with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
illuminate/contracts Version ^11.0
illuminate/database Version ^11.0
illuminate/support Version ^11.0
illuminate/events Version ^11.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package alifaraun/wallet contains the following files

Loading the files please wait ...