Zum Inhalt springen

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.

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.