
# Повтори

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/uk/docs/workflows/conditional)
- [Сигнали](https://queuety.inline0.dev/uk/docs/workflows/signals)
- [Обмеження](https://queuety.inline0.dev/uk/docs/workflows/guardrails)
- [PHP API](https://queuety.inline0.dev/uk/docs/api)
