NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Credits for Laravel

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 off

Vlastná 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 kryptomien

Odoslaní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.