
# Динамический цикл For-Each

Используйте `->for_each()`, если во время построения рабочего процесса количество ветвей неизвестно. Queuety считывает массив из состояния рабочего процесса, запускает отдельное задание ветви для каждого элемента и продолжает выполнение только после выполнения заданного условия завершения.

## Метод `->for_each()`

```php
use Queuety\Contracts\ForEachHandler;
use Queuety\Contracts\ForEachReducer;
use Queuety\Enums\ForEachMode;
use Queuety\Queuety;

Queuety::workflow( 'index_site' )
    ->then( PlanPagesStep::class )
    ->for_each(
        items_key: 'pages',
        handler_class: IndexPageStep::class,
        result_key: 'page_results',
        mode: ForEachMode::Quorum,
        quorum: 2,
        reducer_class: IndexSummaryReducer::class,
        name: 'index_pages',
    )
    ->then( PublishIndexStep::class )
    ->dispatch( [ 'site_id' => 42 ] );
```

Шаг планировщика записывает `pages` в состояние рабочего процесса. Шаг цикла for-each считывает этот массив и запускает отдельное задание ветви для каждого элемента.

## Обработчики ветвей

Обработчики ветвей цикла for-each реализуют `Contracts\ForEachHandler`:

```php
use Queuety\Contracts\ForEachHandler;

final class IndexPageStep implements ForEachHandler {
    public function handle_item( array $state, mixed $item, int $index ): array {
        $page = is_array( $item ) ? $item : [ 'url' => (string) $item ];

        return [
            'url' => $page['url'],
            'title' => fetch_title( $page['url'] ),
            'index' => $index,
        ];
    }

    public function config(): array {
        return [
            'max_attempts' => 3,
            'backoff' => 'exponential',
        ];
    }
}
```

В `config()` параметр `backoff` принимает имя стратегии: `exponential`, `linear` или `fixed`. Список задержек для отдельных попыток в этом месте сейчас отбрасывается, поэтому повторные попытки выполняются без задержки ([#16](https://github.com/inline0/queuety/issues/16)); задавайте задержки для отдельных попыток в `backoff` определения шага.

`$state` содержит полное состояние рабочего процесса, `$item` — отдельный элемент цикла for-each, а `$index` — его позицию, начиная с нуля.

## Режимы завершения

`ForEachMode::All`

Шаг завершается, только когда успешно выполнены все ветви. Любой окончательный сбой ветви приводит к сбою цикла for-each.

`ForEachMode::FirstSuccess`

Шаг завершается, как только успешно выполнена первая ветвь. Остальные активные ветви помечаются как вытесненные.

`ForEachMode::Quorum`

Шаг завершается, когда успешно выполнено не менее `quorum` ветвей. Если оставшиеся ветви уже не могут обеспечить кворум, рабочий процесс завершается с ошибкой.

## Редукторы

По умолчанию Queuety сохраняет структурированный агрегат в `result_key`:

- `mode`
- `quorum`
- `total`
- `succeeded`
- `failed`
- `settled`
- `winner`
- `results`
- `failures`

Чтобы создать собственную сводку, передайте класс редуктора, реализующий `Contracts\ForEachReducer`. Агрегат по-прежнему сохраняется в `result_key`, а возвращенные редуктором ключи объединяются с состоянием рабочего процесса рядом с ним:

```php
use Queuety\Contracts\ForEachReducer;

final class IndexSummaryReducer implements ForEachReducer {
    public function reduce( array $state, array $for_each ): array {
        return [
            'indexed_count' => count( $for_each['results'] ),
            'winning_url' => $for_each['winner']['output']['url'] ?? null,
        ];
    }
}
```

Результат редуктора объединяется с состоянием рабочего процесса так же, как результат любого другого шага. Он также может возвращать `_next_step` для перехода к ветвлению после завершения цикла for-each.

## Повторные попытки

Для ветвей for-each повторные попытки выполняются независимо согласно `config()` обработчика ветви. Если шаг for-each завершается с ошибкой и вы вызываете `retry_workflow()`, Queuety повторно разворачивает шаг for-each и ставит в очередь только отсутствующие или завершившиеся с ошибкой ветви. Результаты успешно выполненных ветвей остаются в состоянии рабочего процесса.

## Выбор между `parallel()` и `for_each()`

Используйте `parallel()`, если список ветвей фиксирован в коде.

Используйте `for_each()`, если список ветвей определяется во время выполнения по состоянию рабочего процесса и нужны режимы завершения, например успех первой ветви или кворум.
