
# Workflow-Signale

Workflows können mithilfe von Signalen pausieren und auf externe Eingaben warten. Die Builder-Methoden `wait_for_signal()` und `wait_for_signals()` fügen Schritte ein, die den Workflow blockieren, bis deine Anwendung, eine zuständige Person oder ein anderes System das erwartete Signal über `Queuety::signal()` sendet.

Für Abläufe mit menschlicher Beteiligung enthält Queuety außerdem:

- `wait_for_approval()` für eine Approval-Nutzlast mit Namensraum
- `wait_for_input()` für strukturierte menschliche Eingaben
- `wait_for_decision()` für ausdrückliche Genehmigungs- oder Ablehnungsergebnisse

Ein ausführlicheres Beispiel von Anfang bis Ende, das Agentenläufe mit menschlichen Freigabeschritten verbindet, findest du unter [Agenten-Orchestrierung](https://queuety.inline0.dev/de/docs/workflows/agent-orchestration).

## Die Methode `->wait_for_signal()`

Füge einem Workflow einen Signalschritt hinzu:

```php
use Queuety\Queuety;

$workflow_id = Queuety::workflow( 'content_approval' )
    ->then( DraftContentHandler::class )
    ->wait_for_signal( 'approved' )
    ->then( PublishContentHandler::class )
    ->dispatch( [ 'post_id' => 99 ] );
```

Wenn der Workflow den Schritt `wait_for_signal` erreicht, erhält er den Status `waiting_for_signal` und pausiert. Während des Wartens laufen keine Jobs und es werden keine Ressourcen verbraucht.

Standardmäßig wird eine einzelne Signalnutzlast mit dem obersten Workflow-Zustand zusammengeführt. Wenn du die Nutzlast stattdessen in einem eigenen Namensraum ablegen möchtest, übergib ein `result_key`:

```php
$workflow_id = Queuety::workflow( 'content_approval' )
    ->then( DraftContentHandler::class )
    ->wait_for_signal( 'approved', 'approval' )
    ->then( PublishContentHandler::class )
    ->dispatch( [ 'post_id' => 99 ] );
```

Nach Eingang des Signals enthält `$state['approval']` das Array der Signalnutzlasten.

## Auf mehrere Signale warten

Verwende `wait_for_signals()`, wenn der Workflow durch eines von mehreren möglichen Signalen fortgesetzt werden soll oder wenn er alle erforderlichen Signale sammeln muss, bevor er weiterläuft.

```php
use Queuety\Enums\WaitMode;

$workflow_id = Queuety::workflow( 'moderation_gate' )
    ->then( SubmitForReviewHandler::class )
    ->wait_for_signals(
        [ 'editor_approved', 'legal_approved' ],
        WaitMode::All,
        'approvals'
    )
    ->then( PublishContentHandler::class )
    ->dispatch();
```

- `WaitMode::All` wartet, bis jedes konfigurierte Signal registriert wurde.
- `WaitMode::Any` setzt den Workflow fort, sobald das erste konfigurierte Signal eintrifft.

Mit einem `result_key` speichert ein Multi-Signal-Warteschritt ein assoziatives Array, dessen Schlüssel die Signalnamen sind:

```php
[
    'editor_approved' => [ 'by' => 'editor@example.com' ],
    'legal_approved' => [ 'by' => 'legal@example.com' ],
]
```

Ohne ein `result_key` werden die Nutzlasten der passenden Signale in der Reihenfolge, in der der Schritt konfiguriert wurde, mit dem obersten Zustand zusammengeführt.

## Abgleich und Zuordnung

Signal-Warteschritte können irrelevante Ereignisse ignorieren, indem sie Nutzlastinhalte abgleichen oder anhand eines gemeinsamen Schlüssels zuordnen.

```php
$workflow_id = Queuety::workflow( 'review_gate' )
    ->wait_for_signal(
        name: 'review_ready',
        result_key: 'review',
        match_payload: [ 'source' => 'editorial' ],
        correlation_key: 'task_id',
    )
    ->dispatch( [ 'task_id' => 'task-42' ] );
```

In diesem Beispiel wird das Warten nur dann beendet, wenn:

- der Signalname `review_ready` lautet
- die Nutzlast `source: editorial` enthält
- `task_id` der Nutzlast mit `$state['task_id']` übereinstimmt

Das ist nützlich, wenn mehrere externe Systeme oder Agenten Ereignisse mit denselben Namen veröffentlichen, aber nur eine Nutzlast zum aktuellen Workflow-Lauf gehört.

## Helfer für menschliche Freigaben und Eingaben

`wait_for_approval()` und `wait_for_input()` sind praktische Wrapper für Signal-Warteschritte. Sie verhalten sich wie `wait_for_signal()`, verwenden aber standardmäßig einen Ergebnisschlüssel mit Namensraum.

```php
$workflow_id = Queuety::workflow( 'blog_review' )
    ->then( GenerateDraftHandler::class )
    ->wait_for_approval()
    ->wait_for_input()
    ->then( HandleDecisionHandler::class )
    ->dispatch();
```

Nach dem Signal `approval` enthält `$state['approval']` die Approval-Nutzlast. Nach dem Signal `input` enthält `$state['input']` die Eingabenutzlast.

Verwende `wait_for_decision()`, wenn ein einzelner Schritt entweder bei einer Genehmigung oder einer Ablehnung fortgesetzt werden soll:

```php
$workflow_id = Queuety::workflow( 'legal_review' )
    ->then( DraftContractHandler::class )
    ->wait_for_decision( result_key: 'review' )
    ->then( HandleReviewDecisionHandler::class )
    ->dispatch();
```

Nach Eingang des Signals enthält `$state['review']`:

```php
[
    'outcome' => 'approved', // or 'rejected'
    'signal'  => 'approved', // or 'rejected'
    'data'    => [ 'reviewer' => 'ops@example.com' ],
]
```

## Ein Signal senden

Sende mit `Queuety::signal()` ein Signal an einen Workflow:

```php
Queuety::signal( $workflow_id, 'approved' );
```

Wenn das Signal dem entspricht, auf das der Workflow wartet, wird der Workflow fortgesetzt und geht zum nächsten Schritt über.

Für Abläufe mit menschlicher Beteiligung bietet die Fassade außerdem semantische Helfer:

```php
Queuety::approve_workflow( $workflow_id, [ 'reviewer' => 'editor@example.com' ] );
Queuety::reject_workflow( $workflow_id, [ 'reason' => 'needs citations' ] );
Queuety::submit_workflow_input( $workflow_id, [ 'notes' => 'Ship after copy edit' ] );
```

### Signaldaten

Du kannst zusammen mit dem Signal ein Daten-Array übergeben. Die Daten werden mit dem Workflow-Zustand zusammengeführt und stehen dadurch den folgenden Schritten zur Verfügung:

```php
Queuety::signal( $workflow_id, 'approved', [
    'approved_by' => 'admin@example.com',
    'approved_at' => time(),
] );
```

Im nächsten Schritt:

```php
class PublishContentHandler implements Step {
    public function handle( array $state ): array {
        // $state['approved_by'] is 'admin@example.com'
        // $state['approved_at'] is the timestamp
        // $state['post_id'] is 99
        wp_update_post( [
            'ID'          => $state['post_id'],
            'post_status' => 'publish',
        ] );
        return [ 'published' => true ];
    }

    public function config(): array {
        return [];
    }
}
```

Schlüssel, die mit `_` (Unterstrich) beginnen, sind für die interne Verwendung reserviert und werden nicht mit dem Zustand zusammengeführt.

## Bereits vorhandene Signale

Signale können gesendet werden, bevor der Workflow den Warteschritt erreicht. Wenn ein Signal eintrifft, wird es unabhängig vom aktuellen Status des Workflows in der Tabelle `queuety_signals` gespeichert. Erreicht der Workflow später den Schritt `wait_for_signal`, sucht er nach bereits vorhandenen Signalen und fährt sofort fort, falls er eines findet.

Das bedeutet, dass die Reihenfolge keine Rolle spielt:

```php
// Dispatch workflow
$workflow_id = Queuety::workflow( 'order_fulfillment' )
    ->then( PrepareOrderHandler::class )
    ->wait_for_signal( 'payment_confirmed' )
    ->then( ShipOrderHandler::class )
    ->dispatch( [ 'order_id' => 123 ] );

// Signal arrives before the workflow reaches the wait step
Queuety::signal( $workflow_id, 'payment_confirmed', [ 'transaction_id' => 'txn_abc' ] );

// When the workflow finishes PrepareOrderHandler and reaches the wait step,
// it sees the pre-existing signal and continues immediately.
```

## Workflow-Status während des Wartens auf ein Signal

Während der Workflow auf ein Signal wartet, lautet sein Status `WorkflowStatus::WaitingForSignal` (`waiting_for_signal`):

```php
$state = Queuety::workflow_status( $workflow_id );
echo $state->status->value; // 'waiting_for_signal'
echo $state->wait_type;     // 'signal'
echo $state->wait_mode;     // 'all' or 'any'
print_r( $state->waiting_for ); // ['approved']
print_r( $state->wait_details ); // matched/remaining, correlation info, result key, ...
```

## Anwendungsfall: menschliche Freigabe

Erstelle einen Freigabe-Workflow, in dem eine prüfende Person Inhalte genehmigen muss, bevor sie veröffentlicht werden:

```php
$workflow_id = Queuety::workflow( 'blog_review' )
    ->then( GenerateDraftHandler::class )
    ->then( NotifyReviewerHandler::class )
    ->wait_for_decision( result_key: 'review' )
    ->then( HandleDecisionHandler::class )
    ->dispatch( [ 'topic' => 'PHP middleware patterns' ] );
```

`NotifyReviewerHandler` sendet eine E-Mail- oder Slack-Nachricht mit Links zum Genehmigen oder Ablehnen. Diese Links rufen deinen Controller auf, der das Signal sendet:

```php
// In your approval controller
Queuety::approve_workflow(
    $workflow_id,
    [
        'reviewer'    => $current_user->user_email,
        'reviewed_at' => time(),
    ],
    'approved'
);
```

Der Schritt `HandleDecisionHandler` liest `$state['review']['outcome']` und veröffentlicht oder archiviert den Entwurf.

## Anwendungsfall: externer API-Callback

Warte auf einen Webhook von einem externen Dienst:

```php
$workflow_id = Queuety::workflow( 'generate_video' )
    ->then( SubmitToRenderServiceHandler::class )
    ->wait_for_signal( 'render_complete' )
    ->then( DownloadVideoHandler::class )
    ->then( NotifyUserHandler::class )
    ->dispatch( [ 'template_id' => 'promo_v2', 'user_id' => 42 ] );
```

Wenn der externe Dienst einen Request an deinen Webhook-Endpunkt sendet:

```php
// In your webhook handler
$payload = json_decode( file_get_contents( 'php://input' ), true );
$workflow_id = (int) $payload['metadata']['workflow_id'];

Queuety::signal( $workflow_id, 'render_complete', [
    'video_url'  => $payload['output_url'],
    'duration'   => $payload['duration_seconds'],
] );
```

## Mehrere Signal-Sperren in einem Workflow

Ein Workflow kann an mehreren Stellen auf Signale warten:

```php
Queuety::workflow( 'multi_approval' )
    ->then( CreateProposalHandler::class )
    ->wait_for_signal( 'manager_approval' )
    ->then( EscalateToDirectorHandler::class )
    ->wait_for_signal( 'director_approval' )
    ->then( ExecuteProposalHandler::class )
    ->dispatch( [ 'proposal_id' => 7 ] );
```

Jeder Signal-Warteschritt ist unabhängig. Du kannst `wait_for_signal()`, `wait_for_signals()`, `wait_for_approval()`, `wait_for_input()` und `wait_for_decision()` im selben Workflow kombinieren.
