
# Повторения

Queuety Рабочие процессы представляют собой упорядоченные цепочки шагов. Используйте управляющий шаг повтора, чтобы вернуться к предыдущему именованному шагу, вместо того чтобы вручную писать логику `_next_step`.

Выберите подходящий для рабочего процесса вариант:

- общедоступный `state_key`, если условие простое
- `condition_class`, если для повтора требуется более сложная логика
- `max_iterations`, если самому повтору нужен жесткий предел

Оба метода указывают на предыдущий именованный шаг, поэтому повтор можно сериализовать и воспроизводить, а среда выполнения рабочего процесса применяет к нему те же ограничения, что и к любому другому шагу.

Условие повтора проверяется по сохраненному общедоступному состоянию рабочего процесса после завершения предыдущего шага, поэтому каждая итерация сохраняется при перезапусках исполнителя.

## `repeat_until()` с ключом состояния

```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` может свести полезную нагрузку решения к простому общедоступному ключу:

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

Если `review_approved` по-прежнему не `true`, шаг повтора возвращается к `draft`. Когда оно становится `true`, рабочий процесс переходит к `PublishBriefStep`.

Сигналы сохраняются, а не потребляются. При втором прохождении через `wait_for_decision()` прежний сигнал `rejected` по-прежнему подходит, и ожидание завершается немедленно, поэтому цикл будет повторяться, пока его не остановит `max_transitions()`. Используйте для каждого раунда свой ключ: пусть `DraftBriefStep` возвращает возрастающее значение `review_round`, передавайте `correlation_key: 'review_round'` в `wait_for_decision()`, а проверяющий отправляет то же `review_round` в полезной нагрузке сигнала. Тогда ожидание завершится только для сигнала, у которого `review_round` совпадает с текущим значением состояния.

## `repeat_until()` с классом условия

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

Классы условий получают общедоступное состояние рабочего процесса, поэтому предикат можно сериализовать и анализировать без замыканий.

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

Этот шаблон полезен, когда шаг записывает простое логическое значение, например `should_poll_again`, `has_more_pages` или `needs_revision`.

## Почему повторы отделены от `_next_step`

Вы по-прежнему можете создавать ветвления вручную, возвращая `_next_step` из обычного шага. Вспомогательные средства повтора предназначены для более узкого случая, когда:

- цель — предыдущий шаг
- условие берется из общедоступного состояния рабочего процесса
- нужно, чтобы логика повтора оставалась видимой в самом определении рабочего процесса

Так повторы проще анализировать в экспортах, журналах событий и документации, чем если бы возврат к предыдущему шагу был спрятан в произвольном коде шага.

## Ограничение повторов

Используйте `max_iterations`, если для конкретного повтора нужен локальный жесткий предел:

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

Используйте `max_transitions()`, если всему рабочему процессу нужен более общий механизм аварийного останова:

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

Если повтор превышает `max_iterations` или рабочий процесс превышает `max_transitions()`, Queuety завершает запуск с ошибкой, не позволяя ему выполняться бесконечно.

## Связанная документация

- [Условное ветвление](https://queuety.inline0.dev/ru/docs/workflows/conditional)
- [Сигналы](https://queuety.inline0.dev/ru/docs/workflows/signals)
- [Ограничения](https://queuety.inline0.dev/ru/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/ru/docs/api)
