
# 动态 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 会重新展开该步骤，并且只将仍缺失或失败的分支重新加入队列。成功分支的结果会保留在工作流状态中。

## 选择 `parallel()` 还是 `for_each()`

如果分支列表在代码中固定，请使用 `parallel()`。

如果分支列表是在运行时从工作流状态中发现的，并且需要首个成功或法定人数等完成模式，请使用 `for_each()`。
