Ochrana pred prečerpaním
Odpis, ktorý by zostatok dostal pod minimum_balance (predvolene nula), sa predvolene odmietne výnimkou InsufficientCreditsException — zostatok nemôže potichu klesnúť do mínusu a nič sa nezapíše. Na fasáde vráti allowOverdraft() scope, ktorý smie klesnúť pod hranicu:
use RoundlyConsulting\Credits\Exceptions\InsufficientCreditsException;
try {
Credits::for($user)->deduct(1000, 'big purchase');
} catch (InsufficientCreditsException $e) {
// $e->requested, $e->available, $e->creditable — nothing was written
}
Credits::for($user)->allowOverdraft()->deduct(1000, 'manual debit'); // may go below the floor
Credits::for($user)->allowOverdraft()->setTo(-50, 'write-off');To isté cez trait:
use RoundlyConsulting\Credits\Exceptions\InsufficientCreditsException;
try {
$user->modifyCredits(-1000, 'big purchase');
} catch (InsufficientCreditsException $e) {
$e->requested; // -1000 — the signed amount that was refused
$e->available; // 70 — the bucket's balance when the guard decided
$e->creditable; // the entity
$e->getMessage(); // 'Insufficient credits: tried to deduct 1000 but only 70 are available.'
}Povolenie prečerpania
Záporný zostatok povolíte pre jedno volanie — cez allowOverdraft() na scope alebo argument allowOverdraft pri metódach traitu:
$user->modifyCredits(-1000, 'manual debit', allowOverdraft: true);
$user->setCreditsTo(-50, 'write-off', allowOverdraft: true);Alebo globálne, konfiguračným kľúčom allow_overdraft či jeho env premennou. Hodnota z env sa číta ako boolean: 1, true, on a yes ho zapnú; 0, false, off, no a čokoľvek nerozpoznané ho nechajú vypnuté:
CREDITS_ALLOW_OVERDRAFT=true # 1, true, on or yes — anything else leaves it offVlastná hranica
minimum_balance je hranica, ktorú ochrana stráži. Zvýšte ju, ak chcete držať rezervu, alebo ju nastavte pod nulu pre ohraničené prečerpanie:
// config/credits.php — a bounded overdraft: balances may fall to -500, never below
'minimum_balance' => -500,Ochrana platí pre každú skupinu zvlášť: odpis zo skupiny purchased nikdy nesiahne na promo kredity. Pripísania ochranou vôbec neprechádzajú.
Súbežnosť
Zostatok nikdy nie je uložený stĺpec — je to súčet knihy pohybov, ktorá sa len dopĺňa. Každá zmena (add, deduct, modify, setTo a ich podoby pre Money) beží v jednej transakcii, ktorá najprv vezme zámok vlastníka — UPDATE jeho riadku v credit_locks — potom načíta záznamy skupiny pod lockForUpdate(), rozhodne (ochrana pred prečerpaním pri odpise, rozdiel pri setTo()) a pridá nový záznam. Súbežné zmeny toho istého vlastníka sa preto zoradia za sebou na každej úrovni izolácie:
- READ COMMITTED (predvolená v Postgres): druhá zmena počká na prvú a potom načíta zostatok, ktorý po sebe prvá nechala — aj v prázdnej skupine so zápornou minimum_balance.
- MySQL / MariaDB pri REPEATABLE READ (ich predvolená úroveň): zámok aj čítanie knihy pohybov sú aktuálne čítania, takže vidia aj potvrdený záznam prvej zmeny.
- Postgres pri REPEATABLE READ / SERIALIZABLE: snímka druhej zmeny je staršia než jej čakanie, preto namiesto rozhodovania nad zastaranými dátami zlyhá na chybe serializácie (SQLSTATE 40001) a balík jej transakciu zopakuje — najviac 5 pokusov — s novou snímkou.
Vo vašej vlastnej transakcii
Vo vašej vlastnej DB::transaction() patrí snímka vašej transakcii, takže balík ju za vás zopakovať nemôže: na Postgres pri REPEATABLE READ / SERIALIZABLE sa súbežná zmena prejaví ako Illuminate\Database\DeadlockException (SQLSTATE 40001) a nič sa nezapíše. Zopakujte celú transakciu — DB::transaction($callback, attempts: 3) to neurobí, pretože vnorená výnimka už nenesie SQLSTATE, podľa ktorého Laravel rozhoduje o opakovaní:
use Illuminate\Database\DeadlockException;
use Illuminate\Support\Facades\DB;
use RoundlyConsulting\Credits\Facades\Credits;
retry(3, fn () => DB::transaction(function () use ($user): void {
// … your own reads and writes …
Credits::for($user)->deduct(30, 'purchase');
}), when: fn (Throwable $e): bool => $e instanceof DeadlockException);Zámok sa drží len počas tejto krátkej transakcie. Keďže každá zmena sa zapíše ako nový rozdielový záznam (nikdy nie absolútny zostatok), žiadna zmena sa nestratí a credit_locks drží len počítadlo verzií — jediným zdrojom pravdy ostáva kniha pohybov.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.