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

Kontrakt vlastných krokov

Typ kroku je trieda, ktorá rozširuje RoundlyConsulting\Workflows\Steps\StepType. Je zároveň položkou katalógu (čo editor zobrazí a overí) aj handlerom (čo sa spustí).

use RoundlyConsulting\Workflows\Steps\Schema\ConfigSchema;
use RoundlyConsulting\Workflows\Steps\StepContext;
use RoundlyConsulting\Workflows\Steps\StepResult;
use RoundlyConsulting\Workflows\Steps\StepType;

final class CreateTaskStep extends StepType
{
    public function key(): string { return 'acme.create_task'; }
    public function label(): string { return __('acme.workflows.create_task'); }
    public function hasSideEffects(): bool { return true; }
    public function ability(): ?string { return 'createTask'; }

    public function configSchema(): array
    {
        return ConfigSchema::object()->string('title', required: true, maxLength: 200)->toArray();
    }

    public function handle(StepContext $context): StepResult
    {
        $taskId = $context->once('task', fn (): int => Task::create([
            'title' => $context->config()['title'],
            'subject_id' => $context->subject()?->getKey(),
        ])->id);

        return StepResult::complete(output: ['task_id' => $taskId]);
    }

    public function simulate(StepContext $context): StepResult
    {
        return StepResult::complete(output: ['task_id' => null])
            ->wouldDo('Create task "'.$context->config()['title'].'"');
    }
}

Registrácia

// config/workflows.php
'steps' => [App\Workflows\CreateTaskStep::class],

// or at runtime, in a service provider
Workflows::catalogue()->register(CreateTaskStep::class);
  • Vlastné kľúče musia obsahovať bodku (acme.create_task); kľúče bez bodky sú vyhradené pre balík: trigger, condition, branch, set_field, transition, delay, wait, loop, start_workflow, end, approval, notify, webhook.
  • Opätovná registrácia tej istej triedy nič nerobí; iná trieda s rovnakým kľúčom vyhodí DuplicateStepTypeException. Nesprávny kľúč alebo neplatná schéma konfigurácie vyhodí pri registrácii InvalidStepTypeException (nepodporované kľúčové slová JSON Schema, vzdialené $ref a neplatné vzory sa odmietnu už tam, s uvedením kľúčového slova).
  • Katalóg sa načíta lenivo, takže aj providery aplikácie, ktoré sa spúšťajú po balíku, sa zaregistrujú včas.

Východiskový súbor si vygenerujete takto:

php artisan make:workflow-step CreateTicketStep
# app/Workflows/Steps/CreateTicketStep.php — key app.create_ticket, a label, side effects on,
# ability() returning null, a configSchema() with one required title string and a handle()
# that completes with empty output; register it with Workflows::catalogue()->register()

Vygenerovaná trieda je jednoduchšia než príklad vyššie: jej handle() obsahuje komentár odkazujúci na once() a idempotencyKey() a chýba jej simulate(). Časti once(), simulate() a ability() z príkladu doplňte sami.

Trieda

MetódaPovinnáVýznam
key(): stringánoKľúč v katalógu; vlastné kľúče musia obsahovať bodku (acme.create_task).
label(): string, description(): ?string, category(): stringlabelPreložený text pre UI. description() je predvolene null, category() je custom.
configSchema(): arrayánoPodmnožina JSON Schema; #/$defs/{value, party, parties, duration, condition, field} sú k dispozícii.
outcomes(array $config): list<Outcome>predvolene [Outcome::next()]Outcome::of($key, required: true, label: null). Môže závisieť od konfigurácie.
outputSchema(): ?arraynieUmožní validácii pri publikovaní overiť odkazy steps.<node>.output.<key>.
hasSideEffects(): boolánoTypy s vedľajšími efektmi dostanú autorizačný hook, opakovania, idempotentné kľúče a „would run“ v skúšobných behoch.
ability(): ?stringnieOprávnenie z policy, ktoré predvolený autorizátor overí na subjekte pred handle().
abilityFor(StepContext $context): ?stringnieOprávnenie pre konkrétny beh (predvolene ability()); set_field používa oprávnenie typu subjektu.
retry(): ?RetryPolicyniePredvolená politika opakovania typu.
supportsEscalations(): boolpredvolene falseescalations na uzle iného typu → invalid_config na /escalations.
validateConfig(array $config, ValidationScope $scope): list<ValidationError>nieSémantické kontroly nad rámec schémy; chyby vytvárajte cez ValidationError::make(…).
ownerSpecificPaths(): list<string>nieJSON pointery, ktoré sa pri importe šablóny vymažú a označia; odvodzujú sa aj z x-workflows-owner-specific: true.
handle(StepContext $context): StepResultánoSamotná práca.
resume(StepContext $context, Signal $signal): StepResulttypy čakajúce na udalosti alebo podmienkyVolá sa, keď čakanie nastane. Predvolene dokončí timer s next a timeout s jeho výsledkom; iný signál zlyhá s resume_unsupported.
simulate(StepContext $context): ?StepResultnieSkúšobný beh. Musí byť čistý. Ak chýba pri type s vedľajšími efektmi → „would run“.
cancel(StepContext $context): voidnieUpratanie pri zrušení inštancie, kým je krok živý (čaká, je vo fronte na opakovanie alebo beží). Musí byť idempotentné.
reassign(StepContext $context, list<PartyReference> $to): ?StepResult + supportsReassign(): boolnieEskalačná akcia reassign (predvolene žiadna; publikovanie odmietne reassign na type bez nej).

Podrobnosti k cancel(): pokus, ktorý bežal, sa uprace dvakrát — raz pri zrušení (bez výstupu) a znova, keď sa jeho výsledok zaznamená a zahodí, s výstupom, ktorý vrátil, alebo po zlyhanom pokuse či spadnutom workeri bez neho: čo once() stihol uložiť, prečítate cez $context->recall().

Písanie schémy konfigurácie

ConfigSchema zapíše schému len s podporovanými kľúčovými slovami, takže zostavená schéma sa vždy zaregistruje. Každá metóda prijíma ?string $description a ?string $label (kľúčové slovo title pre formuláre editora). Schéma je nemenná — každé volanie vráti novú:

use RoundlyConsulting\Workflows\Steps\Schema\ConfigSchema;

public function configSchema(): array
{
    return ConfigSchema::object()                                       // additionalProperties: false (object(true) allows more)
        ->string('title', required: true, maxLength: 200, label: 'Title')
        ->party('assignee', ownerSpecific: true)                        // flagged for review on template import
        ->enum('priority', ['low', 'normal', 'high'], default: 'normal')
        ->integer('estimate', min: 1, max: 40)
        ->list('tags', 'string', maxItems: 10)
        ->nested('meta', ConfigSchema::object()->string('source'))
        ->duration('due_in')
        ->nodeId('after')                                               // another node's id, remapped on import
        ->toArray();
}
MetódaVlastnosť
string($name, required, minLength, maxLength, pattern, format, description, label){type: string, …}
integer($name, required, min, max, …), number(…), boolean($name, required, default, …){type: integer|number|boolean, minimum?, maximum?}
enum($name, list $values, required, default, …){enum, default?}
list($name, ConfigSchema|string $items, required, minItems, maxItems, …){type: array, items} (reťazec je JSON typ)
nested($name, ConfigSchema $schema, required, …)Vnorený objekt (object() je statický vstupný bod, preto nested()).
value, party(…, ownerSpecific), parties(…, ownerSpecific), duration, condition, field{$ref: "#/$defs/<name>"} (+ x-workflows-owner-specific: true)
nodeId($name, …){type: string, format: node-id}

Čo engine zaručuje

  • Inštancia sa nikdy neposunie dvakrát paralelne. Handler nikdy nebeží súbežne s iným krokom tej istej inštancie, ak worker vynucuje timeout jobu (engine.lease − 30 s; potrebuje pcntl) a retry_after spojenia frontu je dlhší než tento timeout. Bez pcntl sa handler, ktorý prekročí lease, môže prekrývať so svojím opakovaním. workflows:doctor kontroluje oboje.
  • $context->idempotencyKey() je rovnaký pre všetky pokusy tej istej návštevy: wf:{instance ulid}:{node id}:{visit} (≤ 100 znakov, nie je tajný). Pre každú návštevu je iný, takže každá iterácia slučky je nový efekt.
  • Pred handle(): subjekt je načítaný v kontexte vlastníka inštancie a overený voči vlastníkovi; typy s vedľajšími efektmi prešli autorizačným hookom; limity sú skontrolované.

Čo musí handler robiť

  • Každý vedľajší efekt urobte idempotentným voči tomuto kľúču. Pri databázových efektoch použite $context->once($name, Closure): spustí closure a jej výsledok uloží v tej istej transakcii, takže opakovanie vráti uložený výsledok. Prípadne kľúč uložte do jedinečného stĺpca. Pri externých efektoch kľúč odovzdajte ďalej (HTTP Idempotency-Key, Notifications::send(idempotencyKey:), idempotencyKey() v lifecycle). once() je práve raz len pre zápisy na tom istom databázovom spojení.
  • $context->recall($name) prečíta, čo once($name) pre návštevu uložil, bez spustenia čohokoľvek (null, ak nič) — pre hook cancel(), ktorého pokus zlyhal skôr, než vrátil výstup.
  • Nikdy nespite, nepollujte ani neblokujte. Ak chcete čakať, vráťte StepResult::wait(WaitSpec).
  • Medzi volaniami si nedržte žiadny stav. Všetko trvalé patrí do output alebo do vašich vlastných tabuliek.
  • Vracajte výstup ako čistý JSON (skaláry, zoznamy, mapy) ≤ steps_output.max_bytes (64 KiB). Modely, objekty, closures, INF/NAN → pokus zlyhá s invalid_output (neopakuje sa).
  • Všetko nad rámec ability() typu autorizujte cez $context->authorize($ability, $arguments). Zamietnutie ukončí krok s authorization_denied (neopakuje sa).

Hlásenie zlyhania

use RoundlyConsulting\Workflows\Steps\{RetryableStepException, StepFailedException};

throw StepFailedException::because('acme.task_board_closed', 'The task board is closed.');      // permanent
throw RetryableStepException::because('acme.rate_limited', 'Try again later.', now()->addMinute()); // retried

$context->authorize('assignTask', [$assignee]);   // a denial fails the step authorization_denied (never retried)
  • StepFailedException::because($code, $message) — trvalé.
  • RetryableStepException::because($code, $message, ?CarbonInterface $retryAfter) — opakuje sa.
  • Akýkoľvek iný Throwable — opakuje sa s error_code = exception. Jeho správa sa ukladá pre operátorov (error_message, záznam krokov v PHP, workflows:retry), no nikdy sa neposiela von: resources namiesto nej zobrazia preložené workflows::failures.exception, pretože správy frameworku bežne obsahujú názvy tried.
  • Obe metódy because() prijímajú hodnotu FailureCode alebo váš vlastný reťazcový kód; vaše kódy ostávajú vo WorkflowInstance::failure() / StepRun::error() obyčajnými reťazcami. failureCode() a RetryableStepException::retryAfter() sprístupňujú, čo engine zaznamená.

StepResult a WaitSpec

Konštruktor / builderVýznam
StepResult::complete(string $outcome = 'next', array $output = [])Hotovo; pokračuje po hrane pre $outcome.
StepResult::wait(WaitSpec $spec, array $output = [])Zaparkuje krok, kým čakanie nenastane.
StepResult::fail(string $code, string $message)Trvalé zlyhanie (rovnaké ako StepFailedException).
->wouldDo(string $description, array $details = [])V simulate(): akcia, ktorú skúšobný beh vypíše.
WaitSpec::until(CarbonInterface $at)Nastane v danom čase.
WaitSpec::event(string $key, array $match = [])Nastane pri emit()/signal() kľúča.
WaitSpec::condition(array $condition, ?Duration $poll = null)Nastane, keď podmienka platí (pri touch subjektu alebo kontrole).
->timeout(Duration|CarbonInterface $after, string $outcome = 'timed_out')Obnoví krok signálom timeout.
->probe()Obnoví krok raz hneď po commite (pre čakania, ktoré už môžu byť splnené).
->alsoAt(CarbonInterface $at)Ďalší časovač (poistka).

StepResult je final readonly s isComplete(), isWait() a isFail(). Typ kroku, ktorý čaká — tu taký, ktorý pošle zmluvu na elektronický podpis, čaká na udalosť podpísania a po piatich dňoch to vzdá:

use RoundlyConsulting\Workflows\DataTransferObjects\Signal;
use RoundlyConsulting\Workflows\Enums\SignalKind;
use RoundlyConsulting\Workflows\Steps\{Outcome, StepContext, StepResult, WaitSpec};

public function outcomes(array $config): array
{
    return [Outcome::of('signed'), Outcome::of('timed_out')];
}

public function supportsEscalations(): bool { return true; }

public function handle(StepContext $context): StepResult
{
    $envelope = $context->once('envelope', fn (): string => $this->esign->send($context->subject(), $context->idempotencyKey()));

    return StepResult::wait(
        WaitSpec::event('contract.signed')->timeout($context->now()->addDays(5)),
        output: ['envelope' => $envelope],
    );
}

public function resume(StepContext $context, Signal $signal): StepResult
{
    if ($signal->kind === SignalKind::Event) {
        return StepResult::complete('signed', output: ['signed_by' => $signal->payload['by'] ?? null]);
    }

    return parent::resume($context, $signal);   // a timer completes with next, a timeout with timed_out
}

Krok, ktorý používa ->probe(), musí v resume() znova prečítať zdroj pravdy (stav dieťaťa, stav kola), nikdy sa nespoliehať len na druh signálu. Signál, ktorý dorazí počas behu probe, ostane prevzatý a doručí sa neskôr, no probe, ktorý dočasne zlyhá, sa opakuje ako probe — práve opätovné čítanie zdroja zaručí, že obe cesty dospejú k rovnakému výsledku.

StepContext

  • Čítanie: instance(), stepRun(), node(), graph(), version(), definitionKey(), config() (overená schémou), subject(), owner(), actor(), input(), trigger(), output(string $nodeId), resolve(mixed $valueOrRef), field(string $path), parties(list<PartyReference>), idempotencyKey(), attempt(), visit(), now(), timezone(), calendar(?string $name), defaultCalendar(), signal() (v resume()), simulating().
  • Efekty: authorize(string $ability, array $arguments = []) (pri simulácii sa preskočí) a once(string $name, Closure $callback) (pri simulácii vráti null); recall(string $name) prečíta uložený výsledok once() bez spustenia čohokoľvek (null, ak nič nie je, alebo pri simulácii).
  • Zaznamenaný vstup: pred spustením handlera každý pokus uloží step_runs.input — konfiguráciu uzla s odkazmi vyhodnotenými v tej chvíli: literály doslovne, odkazy a relatívne hodnoty vyhodnotené, modely ako {id, type}, strany field a triggered_by doplnené o {id, type} vyhodnotenej osoby (strany role ostanú tak, ako sú zapísané). Nad steps_input.max_bytes sa uloží {"truncated": true, "bytes": n}. Opakovanie si zaznamená vlastný vstup; obnovenie ho ponechá. Tajné údaje do konfigurácie uzlov ani odkazov nedávajte: vstup sa ukladá a StepRunResource ho zobrazuje.

ValidationScope

Voči čomu validateConfig() kontroluje (final readonly): graph, node, subjectType a ownerType (?SubjectTypeDefinition), owner, inputs (deklarované vstupy spúšťača, názov → FieldType), definitionKey; forNode(Node $node) a isAncestor(string $nodeId) — či daný uzol beží pred týmto na každej ceste, takže sa smie odkazovať na jeho výstup. Chyby vytvárajte cez ValidationError::make(ValidationCode, node:, path:, params:).

Podporovaná podmnožina schémy

Draft 2020-12, takže UI môže exportované schémy odovzdať akémukoľvek rendereru formulárov z JSON Schema. Chyby sa vracajú ako ValidationError(code: invalid_config, node, path: JSON pointer, params: {keyword, expected}). Kontroly hĺbky a celkového rozsahu sú obmedzené, takže patologická schéma publikovanie nezasekne:

  • type (jeden alebo zoznam), enum, const.
  • properties, required, additionalProperties (bool alebo schéma), items, minItems, maxItems, uniqueItems.
  • minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf.
  • pattern (napísaný aplikáciou; skompiluje sa raz, s ochranou dĺžky).
  • oneOf, anyOf, allOf, $defs, lokálny $ref (len #/$defs/…).
  • format: date, date-time, duration, timezone, cron, field-path, node-id.
  • Prenesú sa bez zmeny: title, description, default, examples, x-workflows-* (x-workflows-field-types, x-workflows-owner-specific).

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.