
# 동적 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`에는 해당 항목의 0부터 시작하는 위치가 들어 있습니다.

## 완료 모드

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

리듀서 출력은 다른 단계 출력과 마찬가지로 워크플로우 상태에 병합됩니다. For-Each가 완료된 뒤 분기할 수 있도록 `_next_step`를 반환할 수도 있습니다.

## 재시도 동작

For-Each 분기는 분기 핸들러의 `config()`에 따라 각각 독립적으로 재시도됩니다. For-Each 단계가 실패한 뒤 `retry_workflow()`를 호출하면 Queuety가 For-Each 단계를 다시 확장하고 아직 누락되었거나 실패한 분기만 다시 대기열에 넣습니다. 성공한 분기 결과는 워크플로우 상태에 유지됩니다.

## `parallel()`와 `for_each()` 중 선택하기

분기 목록이 코드에 고정되어 있다면 `parallel()`를 사용하세요.

분기 목록을 런타임에 워크플로우 상태에서 찾으며 첫 성공이나 정족수 같은 완료 모드가 필요하다면 `for_each()`를 사용하세요.
