
# Répétitions

Queuety workflows sont des chaînes d’étapes ordonnées. Utilisez une étape de contrôle de répétition pour revenir à une étape nommée précédente, plutôt que d’écrire la logique `_next_step` à la main.

Choisissez la forme qui correspond au workflow :

- une `state_key` publique si la condition est simple
- une `condition_class` si la répétition nécessite une logique plus élaborée
- `max_iterations` si la répétition elle-même nécessite une limite stricte

Les deux méthodes ciblent une étape nommée antérieure : la répétition reste donc sérialisable, rejouable et soumise aux mêmes protections du moteur d’exécution que toutes les autres étapes.

La condition de répétition est évaluée à partir de l’état public persistant du workflow, une fois l’étape précédente terminée. Chaque itération reste ainsi durable même après le redémarrage des workers.

## `repeat_until()` avec une clé d’état

```php
Queuety::workflow( 'approval_revision_loop' )
    ->max_transitions( 12 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class, 'review_outcome' )
    ->repeat_until( 'draft', 'review_approved', true, 'repeat_draft_until_approved' )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

`NormalizeReviewOutcomeStep` peut réduire la charge utile de décision à une simple clé publique :

```php
class NormalizeReviewOutcomeStep implements \Queuety\Step {
    public function handle( array $state ): array {
        return [
            'review_approved' => 'approved' === ( $state['review']['outcome'] ?? null ),
        ];
    }

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

Si `review_approved` n’est toujours pas `true`, l’étape de répétition revient à `draft`. Dès qu’il devient `true`, le workflow passe à `PublishBriefStep`.

Les signaux sont stockés, pas consommés. Au deuxième passage par `wait_for_decision()`, le signal `rejected` précédent correspond toujours, et l’attente se termine aussitôt : la boucle tournerait donc jusqu’à ce que `max_transitions()` l’arrête. Attribuez une clé distincte à chaque tour : demandez à `DraftBriefStep` de renvoyer un `review_round` incrémenté, transmettez `correlation_key: 'review_round'` à `wait_for_decision()` et demandez à la personne chargée de la révision d’envoyer le même `review_round` dans la charge utile du signal. L’attente ne se termine alors que si le `review_round` du signal correspond à la valeur actuelle de l’état.

## `repeat_until()` avec une classe de condition

```php
use App\Workflows\ReviewApprovedCondition;

Queuety::workflow( 'approval_revision_loop' )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->repeat_until(
        target_step: 'draft',
        condition_class: ReviewApprovedCondition::class,
        max_iterations: 5,
        name: 'repeat_draft_until_approved',
    )
    ->then( PublishBriefStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

```php
namespace App\Workflows;

use Queuety\Contracts\RepeatCondition;

final class ReviewApprovedCondition implements RepeatCondition {
    public function matches( array $state ): bool {
        return 'approved' === ( $state['review']['outcome'] ?? null );
    }
}
```

Les classes de condition reçoivent l’état public du workflow. Le prédicat reste ainsi sérialisable et inspectable, sans dépendre de fermetures.

## `repeat_while()`

```php
Queuety::workflow( 'poll_remote_status' )
    ->max_transitions( 20 )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->delay( seconds: 30 )
    ->repeat_while( 'poll', 'should_poll_again', true, 'repeat_poll' )
    ->then( FinalizeImportStep::class )
    ->dispatch( [ 'import_id' => 99 ] );
```

Ce modèle est utile lorsqu’une étape écrit une valeur booléenne simple, comme `should_poll_again`, `has_more_pages` ou `needs_revision`.

## Pourquoi les répétitions sont distinctes de `_next_step`

Vous pouvez toujours créer des branches manuellement en renvoyant `_next_step` depuis une étape ordinaire. Les aides à la répétition sont prévues pour le cas plus précis où :

- la cible est une étape antérieure
- la condition provient de l’état public du workflow
- vous souhaitez que la logique de répétition reste visible dans la définition du workflow

Les répétitions sont ainsi plus faciles à examiner dans les exports, les journaux d’événements et la documentation que si le retour en arrière était enfoui dans du code d’étape arbitraire.

## Encadrer les répétitions

Utilisez `max_iterations` lorsqu’une répétition précise nécessite une limite stricte locale :

```php
Queuety::workflow( 'poll_remote_status' )
    ->then( PollRemoteStatusStep::class, 'poll' )
    ->repeat_while( 'poll', 'should_poll_again', true, max_iterations: 8 )
    ->dispatch();
```

Utilisez `max_transitions()` lorsque l’ensemble du workflow nécessite un mécanisme d’arrêt global :

```php
Queuety::workflow( 'safe_revision_loop' )
    ->max_transitions( 10 )
    ->then( DraftBriefStep::class, 'draft' )
    ->wait_for_decision( result_key: 'review' )
    ->then( NormalizeReviewOutcomeStep::class )
    ->repeat_until( 'draft', 'review_approved', true )
    ->dispatch();
```

Si une répétition dépasse `max_iterations`, ou si le workflow dépasse `max_transitions()`, Queuety interrompt l’exécution au lieu de la laisser tourner indéfiniment.

## Documentation associée

- [Branchement conditionnel](https://queuety.inline0.dev/fr/docs/workflows/conditional)
- [Signaux](https://queuety.inline0.dev/fr/docs/workflows/signals)
- [Garde-fous](https://queuety.inline0.dev/fr/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/fr/docs/api)
