Aller au contenu

Signaux de workflow

Les workflows peuvent se mettre en pause et attendre une entrée externe au moyen de signaux. Les méthodes de génération wait_for_signal() et wait_for_signals() insèrent des étapes qui bloquent le workflow jusqu’à ce que votre application, un opérateur ou un autre système envoie le signal attendu via Queuety::signal().

Pour les workflows avec intervention humaine, Queuety inclut également :

  • wait_for_approval() pour une charge utile d’approbation avec espace de noms
  • wait_for_input() pour une saisie humaine structurée
  • wait_for_decision() pour des résultats explicites d’approbation ou de rejet

Pour un exemple complet plus détaillé combinant des exécutions d’agents et des validations humaines, consultez Orchestration d’agents.

La méthode ->wait_for_signal()

Ajoutez une étape d’attente de signal à un workflow :

PHP
use Queuety\Queuety;

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

Lorsque le workflow atteint l’étape wait_for_signal, son statut devient waiting_for_signal et il se met en pause. Aucun travail n’est en cours et aucune ressource n’est consommée pendant l’attente.

Par défaut, la charge utile d’un signal unique est fusionnée dans l’état de premier niveau du workflow. Pour placer plutôt la charge utile dans un espace de noms, passez un result_key :

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

Après l’arrivée du signal, $state['approval'] contient le tableau des charges utiles des signaux.

Attendre plusieurs signaux

Utilisez wait_for_signals() lorsque le workflow doit se débloquer à la réception de plusieurs signaux possibles, ou lorsqu’il doit recueillir tous les signaux requis avant de continuer.

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 attend que tous les signaux configurés aient été enregistrés.
  • WaitMode::Any reprend dès l’arrivée du premier signal configuré.

Avec un result_key, une attente de plusieurs signaux stocke un tableau associatif indexé par nom de signal :

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

Sans result_key, les charges utiles des signaux correspondants sont fusionnées dans l’état de premier niveau, dans l’ordre de configuration de l’étape.

Correspondance et corrélation

Les attentes de signal peuvent ignorer les événements sans rapport en faisant correspondre le contenu de la charge utile ou en utilisant une clé commune pour la corrélation.

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' ] );

Dans cet exemple, l’attente ne reprend que lorsque :

  • le nom du signal est review_ready
  • la charge utile contient source: editorial
  • la valeur task_id de la charge utile correspond à $state['task_id']

C’est utile lorsque plusieurs systèmes externes ou agents publient les mêmes noms d’événements, mais qu’une seule charge utile correspond à l’exécution du workflow en cours.

Aides à l’approbation et à la saisie humaines

wait_for_approval() et wait_for_input() sont des raccourcis autour des attentes de signal. Ils se comportent comme wait_for_signal(), mais utilisent par défaut une clé de résultat avec espace de noms.

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

Après le signal approval, $state['approval'] contient la charge utile d’approbation. Après le signal input, $state['input'] contient la charge utile de saisie.

Utilisez wait_for_decision() lorsqu’une seule étape doit reprendre après une approbation ou un rejet :

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

Après l’arrivée du signal, $state['review'] contient :

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

Envoyer un signal

Envoyez un signal à un workflow à l’aide de Queuety::signal() :

PHP
Queuety::signal( $workflow_id, 'approved' );

Lorsque le signal correspond à celui attendu par le workflow, celui-ci reprend et passe à l’étape suivante.

Pour les workflows avec intervention humaine, la façade propose également des aides sémantiques :

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' ] );

Données du signal

Vous pouvez transmettre un tableau de données avec le signal. Les données sont fusionnées dans l’état du workflow et deviennent accessibles aux étapes suivantes :

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

À l’étape suivante :

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 [];
    }
}

Les clés commençant par _ (trait de soulignement) sont réservées à un usage interne et ne sont pas fusionnées dans l’état.

Signaux préexistants

Les signaux peuvent être envoyés avant que le workflow n’atteigne l’étape d’attente. À l’arrivée d’un signal, il est enregistré dans la table queuety_signals, quel que soit le statut actuel du workflow. Lorsque celui-ci atteint ensuite l’étape wait_for_signal, il recherche les signaux préexistants et continue immédiatement s’il en trouve un.

L’ordre n’a donc pas d’importance :

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.

Statut du workflow pendant l’attente d’un signal

Pendant l’attente d’un signal, le statut du workflow est 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, ...

Cas d’usage : approbation humaine

Créez un workflow d’approbation où une personne chargée de la révision doit approuver le contenu avant sa publication :

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' ] );

Le NotifyReviewerHandler envoie un e-mail ou un message Slack avec des liens d’approbation ou de rejet. Ces liens appellent votre contrôleur, qui envoie le signal :

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

L’étape HandleDecisionHandler lit $state['review']['outcome'] et publie le brouillon ou le met en archive.

Cas d’usage : rappel externe de API

Attendez un webhook d’un service externe :

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 ] );

Lorsque le service externe envoie une requête à votre point de terminaison webhook :

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'],
] );

Plusieurs étapes d’attente de signal dans un workflow

Un workflow peut attendre des signaux à plusieurs étapes :

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 ] );

Chaque étape d’attente de signal est indépendante. Vous pouvez combiner wait_for_signal(), wait_for_signals(), wait_for_approval(), wait_for_input() et wait_for_decision() dans un même workflow.