---
title: "Kontrakt vlastných krokov — Workflows for Laravel | Roundly"
description: "Napíšte vlastný typ kroku: registrácia, trieda StepType, ConfigSchema, StepContext, StepResult a WaitSpec, idempotencia, zlyhania a hook cancel."
url: https://roundly-consulting.com/sk/open-source/navody/workflows-for-laravel/vlastne-kroky
language: sk
---

[Všetky balíky](https://roundly-consulting.com/sk/open-source.md)

[Workflows for Laravel](https://roundly-consulting.com/sk/open-source/navody/workflows-for-laravel.md)

# 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í).

```php
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

```php
// 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:

```bash
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óda | Povinná | Význam |
| --- | --- | --- |
| `key(): string` | áno | Kľúč v katalógu; vlastné kľúče musia obsahovať bodku (acme.create\_task). |
| `label(): string, description(): ?string, category(): string` | label | Preložený text pre UI. description() je predvolene null, category() je custom. |
| `configSchema(): array` | áno | Podmnož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(): ?array` | nie | Umožní validácii pri publikovaní overiť odkazy steps.<node>.output.<key>. |
| `hasSideEffects(): bool` | áno | Typy s vedľajšími efektmi dostanú autorizačný hook, opakovania, idempotentné kľúče a „would run“ v skúšobných behoch. |
| `ability(): ?string` | nie | Oprávnenie z policy, ktoré predvolený autorizátor overí na subjekte pred handle(). |
| `abilityFor(StepContext $context): ?string` | nie | Oprávnenie pre konkrétny beh (predvolene ability()); set\_field používa oprávnenie typu subjektu. |
| `retry(): ?RetryPolicy` | nie | Predvolená politika opakovania typu. |
| `supportsEscalations(): bool` | predvolene false | escalations na uzle iného typu → invalid\_config na /escalations. |
| `validateConfig(array $config, ValidationScope $scope): list<ValidationError>` | nie | Sémantické kontroly nad rámec schémy; chyby vytvárajte cez ValidationError::make(…). |
| `ownerSpecificPaths(): list<string>` | nie | JSON pointery, ktoré sa pri importe šablóny vymažú a označia; odvodzujú sa aj z x-workflows-owner-specific: true. |
| `handle(StepContext $context): StepResult` | áno | Samotná práca. |
| `resume(StepContext $context, Signal $signal): StepResult` | typy čakajúce na udalosti alebo podmienky | Volá 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): ?StepResult` | nie | Skúšobný beh. Musí byť čistý. Ak chýba pri type s vedľajšími efektmi → „would run“. |
| `cancel(StepContext $context): void` | nie | Upratanie 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(): bool` | nie | Eskalač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ú:

```php
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óda | Vlastnosť |
| --- | --- |
| `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

```php
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 / builder | Vý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á:

```php
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](https://roundly-consulting.com/sk/podporte-nas.md)

Odoslaním daru súhlasíte s našimi [podmienkami prijímania darov](https://roundly-consulting.com/sk/podmienky-darov.md).

[Podporte našu open-source prácu (otvorí sa v novej karte)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Pridajte sa k nám na Patreone (otvorí sa v novej karte)](https://www.patreon.com/cw/roundly)

## 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.

[Ponuka do 48 hodín](https://roundly-consulting.com/sk/kontakt.md) [Prezrieť všetky balíky](https://roundly-consulting.com/sk/open-source/navody/workflows-for-laravel.md)
