Ochrana rozhodnutí
Štyri voliteľné ochrany, od verzie 2.2.0, pre kolá, ktorých schvaľovatelia alebo subjekt sa môžu po otvorení zmeniť. Ak žiadnu nepoužívate, nič sa nemení: konfigurácia nie je potrebná, existujúce kolá a rozhodnutia fungujú ďalej a migrácia 0008 len pridá nullable stĺpce (pozri Inštalácia).
| Ochrana | Zapnete ju cez |
|---|---|
| Vylúčenie aktérov z rozhodovania | ->excluding([...]) na kole a/alebo konfiguračný kľúč exclusions.resolver |
| Naviazanie kola na jednu verziu subjektu | subjekt implementuje ProvidesApprovalFingerprint; fingerprint.on_change |
| Výmena schvaľovateľov jednostupňového kola | ->replaceFlatApprovers([...]) |
| Kontrola, či kolo ešte môže dosiahnuť prah | ->recheckThreshold(?$closeAs) |
Všetky zapisujú pod zámkom riadka kola — tým istým, ktorý si pred zápisom berie každé rozhodnutie. Pozri Súbežnosť nižšie.
Vylúčenie aktérov z rozhodovania
Nedovoľte rozhodovať niekomu, kto je na výsledku zainteresovaný — žiadateľovi kola alebo autorovi schvaľovaného modelu. Pre jedno kolo odovzdajte aktérov do excluding() pri jeho otvorení; uložia sa na kole:
use RoundlyConsulting\Approvals\Facades\Approvals;
// Keep the requester from approving their own invoice
Approvals::request($invoice)
->excluding([auth()->user()])
->from([$controller, $cfo])
->quorum(1)
->open();
// Carries over to a workflow preset — or set it after workflow()
Approvals::request($invoice)->excluding([$requester])->workflow('payout')->open([$a, $b]);
Approvals::request($invoice)->workflow('payout')->excluding([$requester])->open([$a, $b]);
Approvals::for($invoice)->as($requester)->approve(); // UnauthorizedApprovalException — excludedKaždé volanie excluding() zoznam nahradí a neuložený aktér vyhodí InvalidApprovalRequestException (unsavedExcludedActor). Rovnaký zoznam prijímajú aj akcie — excluded v ApprovalRequestData a argument excluded akcií RequestStagedApprovalAction a OpenWorkflowRequestAction.
Pre celú aplikáciu nasmerujte exclusions.resolver na triedu implementujúcu ResolvesExcludedApprovers. Získava sa z kontajnera pri každej kontrole, takže môže mať závislosti v konštruktore:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Approvals\Interfaces\ResolvesExcludedApprovers;
use RoundlyConsulting\Approvals\Models\ApprovalRequest;
final class AuthorsCannotApprove implements ResolvesExcludedApprovers
{
/** @return iterable<Model> */
public function excludedApprovers(Model $approvable, ?ApprovalRequest $request): iterable
{
return $approvable instanceof Invoice && $approvable->author !== null ? [$approvable->author] : [];
}
}
// config/approvals.php
'exclusions' => ['resolver' => AuthorsCannotApprove::class],$request je kolo, do ktorého rozhodnutie smeruje — null pri samostatnom rozhodnutí a počas otvárania kola. Položky, ktoré nie sú uložené modely, sa preskočia. Prázdna hodnota alebo null = bez resolvera; čokoľvek, čo nie je implementácia, vyhodí InvalidConfigurationException s názvom kľúča. Čo vylúčený aktér nesmie, nech pochádza z ktoréhokoľvek zdroja:
| Vylúčený aktér sa pokúsi… | Výsledok |
|---|---|
| approve(), reject(), ask() alebo zapnúť prepínač | UnauthorizedApprovalException (excluded) |
| rozhodnúť ako zástupca menovaného schvaľovateľa | UnauthorizedApprovalException (excluded) |
| nechať za seba rozhodnúť zástupcu — v kole bez menovaných schvaľovateľov, kde aktívne delegovanie vedie zástupcu k rozhodnutiu za delegujúceho | UnauthorizedApprovalException (excludedPrincipal) |
| byť uvedený medzi schvaľovateľmi pri otvorení kola alebo cez replaceApprovers() / replaceFlatApprovers() | InvalidApprovalRequestException (excludedApprover) |
| stiahnuť cez cancel() rozhodnutie, ktoré drží — zapísané ešte pred vylúčením | povolené: stiahnutie len odstráni jeho vplyv |
Zástupca, ktorý zastupuje vylúčeného schvaľovateľa aj iného menovaného schvaľovateľa, rozhodne za toho druhého. Vylúčenia cez resolver platia aj pre samostatné rozhodnutia. Už zapísané rozhodnutia nerušia: rozhodnutie urobené pred vylúčením sa počíta, kým ho aktér nestiahne.
Naviazanie kola na jednu verziu subjektu
Schválenie platí pre to, čo schvaľovateľ videl. Keď sa subjekt neskôr zmení — suma faktúry, text zmluvy —, kolo by sa nemalo počítať ako schválenie novej verzie. Implementujte na subjekte ProvidesApprovalFingerprint. Odtlačok je ľubovoľný reťazec do 255 znakov, ktorý sa zmení vždy, keď sa zmení časť, ktorú schvaľovatelia odsúhlasujú — číslo verzie, pečiatka updated_at alebo vami vypočítaný digest:
use RoundlyConsulting\Approvals\Interfaces\ProvidesApprovalFingerprint;
final class Invoice extends Model implements ProvidesApprovalFingerprint
{
public function approvalFingerprint(): string
{
return sha1($this->amount.'|'.$this->currency.'|'.$this->payee_id);
}
}Dlhšia hodnota vyhodí InvalidApprovalRequestException (fingerprintTooLong). Kolo si odtlačok uloží pri otvorení a každé rozhodnutie aj vyžiadanie uloží ten, pri ktorom vzniklo, takže každý súhlas zaznamená verziu, ku ktorej bol daný. Hodnota sa číta z čerstvej kópie subjektu, takže zastaraný model v pamäti nikdy nepotvrdí verziu, ktorá sa v databáze zmenila. Subjekt bez rozhrania uloží null a kolo otvorené bez odtlačku — alebo pred verziou 2.2.0 — sa nikdy nejaví ako zmenené.
use RoundlyConsulting\Approvals\Facades\Approvals;
Approvals::subjectChanged($invoice); // bool — the open round's fingerprint differs
Approvals::for($invoice)->voidIfChanged(); // int — rounds voided
Approvals::for($invoice)->within($round)->voidIfChanged(); // only that roundvoidIfChanged() uzavrie každé otvorené kolo, ktorého uložený odtlačok sa líši od aktuálneho odtlačku subjektu, ako zrušené, rovnako ako close() — jeho nevybavené vyžiadania sa zrušia, spustia sa ApprovalRequestResolved a ApprovalStatusChanged — a potom spustí ApprovalRequestVoided s uloženým aj aktuálnym odtlačkom. Kolá, ktoré sa ešte zhodujú, kolá bez odtlačku a uzavreté kolá nechá tak; kolo po termíne namiesto toho vyprší ako expirované. Volajte ho z observera updated na subjekte alebo predtým, než podľa schválenia konáte.
Rozhodnutia o zmenenom subjekte sa riadia fingerprint.on_change. Stiahnutia — cancel() a vypnutie prepínača — sa nikdy neodmietnu:
| fingerprint.on_change | Rozhodnutie v kole, ktorého subjekt sa zmenil |
|---|---|
ignore | sa zapíše ako doteraz a uloží odtlačok, pri ktorom vzniklo — predvolené |
refuse | vyhodí ChangedApprovalSubjectException ($e->voided je false); kolo zostane otvorené |
void | kolo zneplatní ako voidIfChanged() a potom vyhodí ChangedApprovalSubjectException ($e->voided je true) |
# .env — what a decision on a changed subject does: ignore (default), refuse or void
APPROVALS_FINGERPRINT_ON_CHANGE=refuseuse RoundlyConsulting\Approvals\Exceptions\ChangedApprovalSubjectException;
use RoundlyConsulting\Approvals\Facades\Approvals;
try {
Approvals::for($invoice)->as($cfo)->approve();
} catch (ChangedApprovalSubjectException $e) {
$e->request; // the round
$e->voided; // true under void — the round was closed as cancelled; false under refuse
}Prázdna hodnota znamená predvolené ignore; akákoľvek iná hodnota vyhodí InvalidConfigurationException. Ďalšia major verzia plánuje ako predvolenú hodnotu refuse, aby subjekt s odtlačkom nikdy neprijal rozhodnutie o verzii, pri ktorej sa jeho kolo neotvorilo.
Výmena schvaľovateľov jednostupňového kola
replaceApprovers() odovzdá iným schvaľovateľom stupeň (pozri Viacstupňové schvaľovanie); replaceFlatApprovers() urobí to isté pre kolo bez stupňov:
use RoundlyConsulting\Approvals\Facades\Approvals;
Approvals::for($invoice)->replaceFlatApprovers([$manager]);
Approvals::for($invoice)->within($round)->replaceFlatApprovers([$manager, $deputy]);- Kolo musí čakať, byť v rámci svojej platnosti a byť jednostupňové — viacstupňové kolo vyhodí InvalidApprovalRequestException (staged) s odkazom na replaceApprovers().
- Nový zoznam tvoria schvaľovatelia, ktorí o kole už rozhodli — platným schválením či zamietnutím —, a za nimi tí, ktorých odovzdáte; každý sa ohodnotí váhou voči subjektu. Pri kole otvorenom bez menovaných schvaľovateľov sa rovnako ponechajú aktéri, ktorí už rozhodli, a kolo sa stane menovaným.
- Kolo, ktoré potrebovalo všetkých menovaných schvaľovateľov, ich potrebuje všetkých aj naďalej; explicitný počet zostane. Pravidlo, kvórum aj expirácia zostanú.
- Nevybavené vyžiadania vyradených schvaľovateľov sa stiahnu — pre každé ApprovalCancelled a ApprovalStatusChanged — a potom sa spustí ApprovalApproversReplaced so stage null a pridanými aj odobranými schvaľovateľmi.
- Kolo nikdy nevyhodnotí a nespúšťa žiadnu autorizačnú Gate: kto smie kolo preradiť, overte vopred.
Odmietne prázdny zoznam (noReplacementApprovers), uzavreté kolo či kolo po termíne (ClosedApprovalRequestException), schvaľovateľa, ktorý už rozhodol (alreadyDecidedRound), vylúčeného schvaľovateľa (excludedApprover), zoznam, pri ktorom by sa kolo nikdy nevyhodnotilo (moreRequiredThanNamed, unreachableThreshold) alebo by sa hneď uzavrelo (replacementSettlesRound), kolo z within() iného subjektu (foreignSubject), subjekt bez kola (noRound) a as(), because(), weight() či expiráciu (settingsOnSubjectCall). Pri odmietnutí sa nič nezmení.
Kontrola, či kolo ešte môže dosiahnuť prah
Schvaľovatelia odchádzajú: ich účet sa zmaže alebo mäkko zmaže, stanú sa vylúčenými alebo prestanú prechádzať kontrolou cez Gate. Kolo postavené na nich sa nemusí nikdy vyhodnotiť. recheckThreshold() overí posledné otvorené kolo subjektu — alebo kolo z within() — a vráti ThresholdVerdict, prípadne null, ak nie je otvorené žiadne kolo (kolo po termíne pritom nechá vypršať ako expirované):
use RoundlyConsulting\Approvals\Enums\ApprovalStatus;
use RoundlyConsulting\Approvals\Facades\Approvals;
$verdict = Approvals::for($invoice)->recheckThreshold(); // check only
$verdict = Approvals::for($invoice)->recheckThreshold(ApprovalStatus::Cancelled); // void if unreachable
$verdict = Approvals::for($invoice)->recheckThreshold(ApprovalStatus::Rejected); // fail if unreachable
if ($verdict !== null && ! $verdict->reachable) {
$verdict->unavailable; // list<NamedApprover> who can no longer decide
$verdict->stage; // the first stage that can no longer clear (staged rounds)
$verdict->closedAs; // Cancelled / Rejected when it closed the round, else null
}Nevybavený menovaný schvaľovateľ — bez platného rozhodnutia — ešte môže rozhodnúť, keď:
- jeho model stále existuje — chýbajúci riadok, mäkko zmazaný model alebo morph typ, ktorý sa už na model nepreloží, znamená, že nemôže;
- nie je vylúčený, zoznamom kola ani resolverom;
- kontrola cez Gate (authorization.enabled) je vypnutá, alebo ňou prejde, alebo má platné delegovanie na zástupcu, ktorý existuje, nie je vylúčený a cez Gate prejde.
Dosiahnuteľné znamená, že platné rozhodnutia spolu s tým, čo títo schvaľovatelia ešte môžu pridať, kolo schvália: jedno schválenie pri Any, potrebný počet bez zamietnutia pri Unanimous, prah podľa váhy pri Quorum a Weighted. Kolo či stupeň bez menovaných schvaľovateľov môže rozhodnúť ktokoľvek, takže sa vždy javí ako dosiahnuteľné. Viacstupňové kolo je dosiahnuteľné, keď každý ešte neuzavretý stupeň môže prejsť; kolo, ktoré pokračuje aj po zamietnutom stupni, je dosiahnuteľné, keď niektorý stupeň už prešiel alebo ešte môže.
S $closeAs — Cancelled alebo Rejected — sa nedosiahnuteľné kolo uzavrie tak, ako ho uzatvára engine (zrušené vyžiadania, ApprovalRequestResolved, ApprovalStatusChanged), a potom sa spustí ApprovalThresholdUnreachable s verdiktom. Dosiahnuteľné kolo zostane otvorené bez ohľadu na $closeAs a akýkoľvek iný výsledok vyhodí InvalidStatusTransitionException (notAnUnreachableOutcome). Spúšťajte ho z plánovaného príkazu alebo po deaktivácii používateľa.
Súbežnosť
Každé rozhodnutie sa vyhodnotí tam, kam smeruje, a zapisuje v transakcii, ktorá si najprv vezme zámok riadka kola. replaceFlatApprovers(), voidIfChanged() aj uzatvárajúce recheckThreshold() si berú ten istý zámok a rozhodujú pod ním, takže:
- rozhodnutie, ktoré svoj cieľ vyhodnotilo pred výmenou a k zámku sa dostane až po nej, si tam schvaľovateľov kola načíta znova — schvaľovateľ, ktorého výmena vyradila, alebo jeho zástupca dostane UnauthorizedApprovalException a nič sa nezapíše;
- rozhodnutie, ktoré sa k zámku dostane po zneplatnení či uzatvárajúcej kontrole, nájde kolo uzavreté a dostane ClosedApprovalRequestException;
- pri on_change refuse alebo void si rozhodnutie pod zámkom znova načíta odtlačok subjektu — subjekt, ktorý sa zmenil po prvej kontrole, sa odmietne tam, a to bez zneplatnenia, keďže zneplatnenie v odmietnutom zápise by sa s ním vrátilo späť;
- zneplatnenie či kontrola, ktoré sa k zámku dostanú až po tom, čo kolo vyhodnotilo rozhodnutie, ho nájdu uzavreté a neurobia nič — nespustí sa žiadna udalosť. Dvaja zapisovatelia, ktorí uzatvárajú to isté kolo, ho uzavrú raz.
Udalosti sa spúšťajú až po commite transakcie.
Fasáda, DI a akcie
replaceFlatApprovers(), voidIfChanged() a recheckThreshold() prijímajú len for() a within(); as(), because(), weight() a expirácia vyhodia InvalidApprovalRequestException (settingsOnSubjectCall). Tie isté volania cez injektovaný manažér a priamo cez akcie:
use RoundlyConsulting\Approvals\Actions\RecheckApprovalThresholdAction;
use RoundlyConsulting\Approvals\Actions\ReplaceRequestApproversAction;
use RoundlyConsulting\Approvals\Actions\VoidChangedApprovalRequestsAction;
use RoundlyConsulting\Approvals\ApprovalsManager;
use RoundlyConsulting\Approvals\Enums\ApprovalStatus;
use RoundlyConsulting\Approvals\Facades\Approvals;
// Facade
Approvals::for($invoice)->recheckThreshold(ApprovalStatus::Cancelled);
// Injected manager
app(ApprovalsManager::class)->for($invoice)->voidIfChanged();
// The raw actions
app(ReplaceRequestApproversAction::class)->execute($invoice, [$manager], $round);
app(VoidChangedApprovalRequestsAction::class)->execute($invoice, $round);
app(RecheckApprovalThresholdAction::class)->execute($invoice, $round, ApprovalStatus::Rejected);Approvals::fake() ich zaznamená ako Replace (so stage null), Void a Recheck — pozri Testovanie.
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.