
# Динамічний цикл 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()`, коли список гілок визначається під час виконання зі стану процесу й потрібні режими завершення на кшталт першого успіху або кворуму.
