
# 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](https://queuety.inline0.dev/fr/docs/workflows/agent-orchestration).

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