Konfigurácia uzla trigger je spúšťačom verzie; pri uložení konceptu sa skopíruje do indexovaných stĺpcov (trigger_type, trigger_key):
| Spúšťač | Konfigurácia | Spôsob spustenia | Idempotentný kľúč |
|---|---|---|---|
manual | inputs: list<{name, type, label?, required?, options?, rules?}> or form: "<forms key>"; optional condition | Workflows::start($key, $subject, $owner)->input([...])->dispatch() | idempotencyKey() volajúceho, inak žiadny. |
event | event: "request.submitted"; optional condition | Workflows::emit(...) or a mapped Laravel event | Kľúč emitu / idempotentný kľúč udalosti. |
schedule | cron: "0 9 * * 1-5" or preset: {every, at?, day?, interval?}; timezone?; for_each? | workflows:wake | schedule:{definition}:{local slot + zone} |
webhook | idempotency: {from: "header:<name>" | "payload:<path>"}; optional subject: {lookup: "<field>", value: {"ref": "payload.<path>"}}; optional condition | Workflows::triggers()->webhook($hookId, $payload, $headers) or the route | Povinný. |
start() slúži len ručným spúšťačom: pri inom type spúšťača vyhodí TriggerMismatchException, takže ručné volanie nikdy neobíde pravidlá idempotencie či podpisu spúšťača. Každé spustenie odmietne vyradenú definíciu (DefinitionNotActiveException) aj subjekt iného vlastníka (SubjectOwnerMismatchException, nič sa nezapíše).
Ručné spustenie
$result = Workflows::start('purchase-approval', $request, $team)
->input(['reason' => 'New laptops'])
->by($user) // the triggering user (required for on_behalf_of)
->idempotencyKey("submit:{$request->id}")
->dispatch(); // StartResult{instance, created, definition}
$result->created; // false when the idempotency key was seen before
$result->wasSkipped(); // the trigger condition was false (skipped: condition_false)
$result->wasRefused(); // the start rate limit refused it — send the same start again later{ "trigger": "manual",
"inputs": [
{ "name": "reason", "type": "string", "label": "Reason", "required": true, "rules": ["max:200"] },
{ "name": "priority", "type": "enum", "options": ["low", "normal", "high"] },
{ "name": "approver", "type": "party", "label": "Approver" } ],
"condition": { "field": "subject.amount", "op": "gt", "value": "0" } }
// or a forms-for-laravel form
{ "trigger": "manual", "form": "purchase-form" }- Vstupy overuje validátor Laravelu: pravidlá vychádzajú z type, required a options, plus z povoleného zoznamu ďalších rules (max:<n>, min:<n>, email, url; čokoľvek iné vrátane regex sa odmietne). Chyby vyhodia InvalidTriggerInputException (422) s errors().
- Typy vstupov sú typy polí (string, text, integer, decimal, boolean, date, datetime, enum, party, list); options obmedzí hodnoty (každá voľba je jedna hodnota; enum s voľbami ich prijme v akomkoľvek skalárnom type).
- Vstup party prijme kľúč používateľa (alebo model) z parties.user_model; neznámy kľúč sa odmietne. input.<name> sa číta ako tento používateľ — v podmienke aj ako strana {kind: field, field: input.<name>} (členstvo vo vlastníkovi stále platí).
- Formuláre (prepojenie s forms): form: "purchase-form" overí vstup cez forms-for-laravel. Ak požiadavku odovzdáte cez ->withForm($request, submit: true), uloží sa odoslanie a jeho uuid sa dostane do payloadu spúšťača.
- Opakovaný idempotencyKey vráti prvú inštanciu (created: false). Bez neho každé volanie spustí novú inštanciu. Spustenie, ktoré odmietne limit (limits.max_starts_per_minute), sa vráti s created: false, refused: 'start_rate_exceeded' a zlyhanou inštanciou a kľúč nespotrebuje: rovnaké spustenie pošlite znova, keď sa v minúte uvoľní miesto.
Udalosti
Workflows::emit('purchase_request.submitted', $request, ['amount' => '1200'], idempotencyKey: "sub:{$request->id}", user: $user);
// or on the subject
$request->workflows()->emit('purchase_request.submitted', ['amount' => '1200']);
$result = Workflows::emit('invoice.paid', $invoice); // EmitResult
$result->started; // list<StartResult> — one per matching active definition
$result->resumed; // how many waiting steps it signalled
$result->skipped(); // starts whose trigger condition was false
$result->failed; // list<EmitFailure{definition, exception}> — definitions that refused to start; the rest still ran
foreach ($result->failed as $failure) {
$failure->definition; // the definition key
$failure->code(); // the exception's errorCode(), e.g. missing_triggering_user
}emit() spustí jednu inštanciu každej aktívnej definície, ktorej aktuálnu verziu spúšťa tento kľúč a ktorá patrí vlastníkovi subjektu (alebo celej aplikácii) a typu subjektu, a signalizuje čakajúcim krokom wait na tento kľúč — len v inštanciách toho istého vlastníka. Definícia, ktorá sa odmietne spustiť (definícia on_behalf_of emitovaná bez používateľa, medzitým vyradená definícia), nezastaví ostatné ani čakania: uvedie sa vo failed ako EmitFailure{definition, exception} s code(). Kľúče udalostí zodpovedajú ^[a-z0-9_]+(\.[a-z0-9_]+)*$; kľúče začínajúce @ sú interné a odmietnu sa (InvalidEventKeyException).
Namapované Laravel udalosti
Laravel udalosť namapujete na emit v metóde boot() service providera alebo v konfigurácii:
use RoundlyConsulting\Workflows\Facades\Workflows;
Workflows::triggers()->map(PurchaseRequestSubmitted::class, 'purchase_request.submitted',
subject: 'request', // the event property holding the subject
user: 'user', // … and the triggering user
payload: ['amount', 'currency'], // public properties, or one method name returning an array
idempotencyKey: 'submissionId', // the property that makes the emit idempotent
);
Workflows::triggers()->listen(InvoiceOverdue::class); // a self-describing WorkflowEvent
Workflows::triggers()->events(); // event basename => trigger key// config/workflows.php — the same, as data
'events' => [
App\Events\RequestSubmitted::class => 'purchase_request.submitted',
App\Events\RequestApproved::class => ['key' => 'purchase_request.approved', 'subject' => 'request', 'user' => 'approver',
'payload' => ['amount'], 'idempotency_key' => 'approvalId'],
App\Events\InvoiceOverdue::class => '*',
],- Payload: zoznam názvov verejných vlastností alebo názov jednej metódy, ktorá vráti pole. Modely v ňom sa zmenia na {id, type} (type = kľúč subjektu v registri, inak krátky názov triedy) — nikdy názov triedy. V podmienkach sa číta ako trigger.payload.<key>.
- Idempotencia: hodnota vlastnosti sa zmení na "{event key}:{value}" (model prispeje svojím kľúčom): tá istá udalosť odoslaná dvakrát spustí jednu inštanciu. Bez nej spustí inštanciu každé odoslanie a workflows:doctor varuje.
- listen() odmietne triedu, ktorá nie je WorkflowEvent (InvalidWorkflowsConfigurationException); map() odmietne '*' ako kľúč.
- Každá namapovaná trieda dostane jeden listener bez ohľadu na to, koľkokrát je namapovaná. Opätovné namapovanie nahradí pôvodné (kód po konfigurácii, vyhráva posledné) a workflows:doctor duplicitu nahlási. Mapovania z providera žijú v singletone, takže prežijú požiadavky v Octane.
- Listener spracuje udalosť až po commite transakcie aplikácie — odvolané podanie nič nespustí a emit nikdy nedrží zámky riadkov vo vnútri transakcie aplikácie.
- Výsledok emitu namapovanej udalosti nikto nečíta, preto sa definícia, ktorá sa odmietne spustiť, nahlási obsluhe výnimiek (do logu) namiesto zlyhania požiadavky aplikácie.
Udalosť, ktorá sa opisuje sama, implementuje Contracts\WorkflowEvent a registruje sa cez listen() alebo mapuje na '*':
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Workflows\Contracts\WorkflowEvent;
final class InvoiceOverdue implements WorkflowEvent
{
public function __construct(public Invoice $invoice, public int $daysOverdue) {}
public function workflowKey(): string { return 'invoice.overdue'; }
public function workflowSubject(): ?Model { return $this->invoice; }
public function workflowPayload(): array { return ['days_overdue' => $this->daysOverdue]; }
public function workflowUser(): ?Model { return null; }
public function workflowIdempotencyKey(): ?string { return "{$this->invoice->id}:{$this->daysOverdue}"; }
}Plány
{ "trigger": "schedule", "cron": "0 9 * * 1-5", "timezone": "Europe/Bratislava" }
{ "trigger": "schedule", "preset": { "every": "weekday", "at": "09:00" } }
// one instance per matching subject each slot
{ "trigger": "schedule", "cron": "0 9 * * 1-5",
"for_each": { "where": { "field": "subject.status", "op": "equals", "value": "open" }, "limit": 200 } }Trigger::schedule('0 9 * * 1-5')->forEach(When::field('subject.status')->equals('open'), limit: 200);
Trigger::schedule(preset: ['every' => 'month', 'at' => '08:00', 'day' => 1], timezone: 'Europe/Bratislava');- cron: päť polí; *, a, a-b, */n, a-b/n, zoznamy; JAN–DEC, SUN–SAT; 0 aj 7 sú nedeľa; makrá @hourly, @daily, @weekly, @monthly, @yearly. Ak je obmedzený deň v mesiaci aj deň v týždni, platí sémantika Vixie. L, W, #, ?, sekundy a roky sa odmietnu (invalid_cron).
- preset: {every: day | weekday | week | month | hour | minutes, at: "09:00", day?, interval?} sa preloží na cron.
- workflows:wake spúšťa splatné termíny v časovom pásme spúšťača. Zmeškané termíny v okne schedule.catch_up (PT1H) sa zlúčia do jedného spustenia. Opakovaná hodina pri prechode času beží raz; vynechaná hodina nebeží.
- Plánované spúšťače nemôžu bežať ako on_behalf_of (actor_requires_user).
- for_each na definícii s typom subjektu spustí pri každom termíne jednu inštanciu pre každý subjekt: každý subjekt tohto typu (pri definícii s vlastníkom len jeho), ktorý spĺňa where, najviac limit — a nikdy viac než wake.batch — na termín. Každé spustenie má kľúč podľa termínu a subjektu. Subjekty sa čítajú v poradí kľúčov a filtrujú v PHP, v kontexte vlastníka (pri definícii tímu Teams::runAs($team), takže model subjektu viazaný na tím vidí riadky tohto tímu, hoci workflows:wake beží bez kontextu): pri veľkej tabuľke radšej použite spúšťač udalosťou.
Webhooky
{ "trigger": "webhook",
"idempotency": { "from": "payload:event.id" }, // or "header:Idempotency-Key"
"subject": { "lookup": "invoice_number", "value": { "ref": "payload.invoice" } } }Workflows::triggers()->webhook($hookId, $payload, $headers) spustí definíciu, ktorej aktuálna verzia má webhook spúšťač s týmto id (webhook_id, vytvorí sa pri prvom publikovaní, obnoví sa cez rotateWebhook()). Idempotentný kľúč je povinný (MissingIdempotencyKeyException, 422) a berie sa z hlavičky alebo cesty v payloade. subject.lookup uvádza registrované pole (nie reláciu), jedinečné v rámci vlastníka, podľa ktorého sa subjekt nájde, a subject.value musí byť {"ref": "payload.<path>"}: hodnota sa prečíta z tela webhooku na danej ceste s bodkami — čítajú sa tu len odkazy payload. a chýbajúca alebo neskalárna hodnota znamená SubjectOwnerMismatchException. Definícia s vlastníkom vezme prvú zhodu, ktorá patrí jej vlastníkovi, a bez zhody nastane SubjectOwnerMismatchException (na route 404).
Prichádzajúca route (voliteľná)
// config/workflows.php
'webhooks' => [
'enabled' => 'on',
'prefix' => 'workflows',
'middleware' => ['api'],
'verify_middleware' => ['sentinel.signed:partners'], // REQUIRED
'max_bytes' => 262144,
],// POST /workflows/hooks/{hook} — route name workflows.webhooks.start
// 202 {instance, created} · 429 start_rate_exceeded (Retry-After: 60) · 413 payload_too_large · 422 · 404
// The same start from PHP (a queue consumer, a test):
$result = Workflows::triggers()->webhook($hookId, $payload, $headers);
Workflows::definition('invoice-intake', $team)->model()->webhook_id; // minted at first publish
Workflows::definition('invoice-intake', $team)->rotateWebhook(); // a new hook idPOST /workflows/hooks/{hook} (názov route workflows.webhooks.start). Je nastavená bezpečne: ak je zapnutá bez verify_middleware, route sa vôbec nezaregistruje (workflows:doctor na tom zlyhá). Odpovede: 202 {instance, created}; 429 {code: start_rate_exceeded, message, instance, created: false} s Retry-After: 60, keď spustenie odmietol limit (kľúč ostane voľný, takže opakovanie od odosielateľa ho spustí); 413 payload_too_large; 422 invalid_payload (nie je JSON objekt) alebo kód balíka; 404 pre neznámu, vyradenú alebo cudziu definíciu (existenciu nikdy nepotvrdí). So sentinel-for-laravel definujte profil podpisu a nastavte verify_middleware = ['sentinel.signed:<profile>']: nepodpísané požiadavky sa odmietnu ešte pred controllerom.
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.