
# Wiederholungen

Queuety-Workflows sind geordnete Schrittketten. Mit einem Wiederholungssteuerungsschritt kannst du zu einem früheren benannten Schritt zurückkehren, statt `_next_step`-Logik von Hand zu schreiben.

Wähle die passende Form für deinen Workflow:

- ein öffentliches `state_key` bei einfachen Bedingungen
- ein `condition_class`, wenn die Wiederholung komplexere Logik erfordert
- `max_iterations`, wenn die Wiederholung selbst unbedingt beendet werden muss

Beide Methoden zielen auf einen früheren benannten Schritt. So bleibt die Wiederholung serialisierbar und erneut abspielbar und wird wie jeder andere Schritt durch dieselbe Workflow-Laufzeitumgebung abgesichert.

Die Wiederholungsbedingung wird nach Abschluss des vorhergehenden Schritts anhand des persistenten öffentlichen Workflow-Status ausgewertet. Dadurch bleibt jede Iteration auch nach einem Neustart des Workers dauerhaft erhalten.

## `repeat_until()` mit einem Statusschlüssel

```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` kann die Entscheidungsdaten in einen einfachen öffentlichen Schlüssel übertragen:

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

Ist `review_approved` weiterhin nicht `true`, springt der Wiederholungsschritt zurück zu `draft`. Sobald der Wert `true` ist, fährt der Workflow mit `PublishBriefStep` fort.

Signale werden gespeichert, nicht verbraucht. Beim zweiten Durchlauf von `wait_for_decision()` stimmt das frühere Signal weiterhin mit `rejected` überein und beendet die Wartezeit sofort, sodass die Schleife laufen würde, bis `max_transitions()` sie stoppt. Verwende für jede Runde einen eigenen Schlüssel: Lass `DraftBriefStep` einen fortlaufenden `review_round` zurückgeben, übergib `correlation_key: 'review_round'` an `wait_for_decision()` und lass den Reviewer denselben `review_round` in der Signalnutzlast senden. Nur ein Signal, dessen `review_round` dem aktuellen Statuswert entspricht, beendet dann die Wartezeit.

## `repeat_until()` mit einer Bedingungsklasse

```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 );
    }
}
```

Bedingungsklassen erhalten den öffentlichen Workflow-Status. So bleibt das Prädikat serialisierbar und prüfbar, ohne von Closures abhängig zu sein.

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

Dieses Muster ist nützlich, wenn ein Schritt einen einfachen booleschen Wert wie `should_poll_again`, `has_more_pages` oder `needs_revision` schreibt.

## Warum Wiederholungen von `_next_step` getrennt sind

Du kannst weiterhin manuell verzweigen, indem du `_next_step` aus einem normalen Schritt zurückgibst. Die Wiederholungshelfer sind für den engeren Anwendungsfall gedacht, wenn:

- das Ziel ein vorheriger Schritt ist
- die Bedingung aus dem öffentlich zugänglichen Workflow-Zustand stammt
- die Wiederholungslogik direkt in der Workflow-Definition sichtbar bleiben soll

Dadurch lassen sich Wiederholungen in Exporten, Ereignisprotokollen und der Dokumentation leichter nachvollziehen, als wenn die Rückkante in beliebigem Schrittcode verborgen ist.

## Wiederholungen absichern

Verwende `max_iterations`, wenn eine bestimmte Wiederholung eine lokale harte Grenze braucht:

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

Verwende `max_transitions()`, wenn der gesamte Workflow einen übergreifenden Notausschalter braucht:

```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();
```

Wenn eine Wiederholung `max_iterations` überschreitet oder der Workflow `max_transitions()` überschreitet, schlägt Queuety den Lauf fehl, statt ihn endlos weiterlaufen zu lassen.

## Verwandte Dokumentation

- [Bedingte Verzweigung](https://queuety.inline0.dev/de/docs/workflows/conditional)
- [Signale](https://queuety.inline0.dev/de/docs/workflows/signals)
- [Schutzmaßnahmen](https://queuety.inline0.dev/de/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/de/docs/api)
