> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 集合

> 說明如何運用 Laravel 的 Collection 類別高效地操作資料。

## 集合是什麼

`Illuminate\Support\Collection` 類別是操作陣列資料的流暢包裝器。
比起分別呼叫 PHP 標準的陣列函式，你可以透過方法鏈直觀地處理資料。

```php theme={null}
// 一般的陣列操作
$names = array_filter(
    array_map(fn ($user) => $user['name'], $users),
    fn ($name) => $name !== null
);

// 使用集合則寫成
$names = collect($users)
    ->pluck('name')
    ->filter()
    ->values();
```

<Info>
  集合是**不可變的**（immutable）。每個方法都不會變更原本的集合，而是回傳新的集合實例。你可以在保留原始資料的同時安全地操作。
</Info>

## 建立集合

### `collect()` 輔助函式

最常用的方式。傳入陣列即可生成集合。

```php theme={null}
use Illuminate\Support\Collection;

// 從陣列建立集合
$users = collect([
    ['name' => '山田太郎', 'age' => 28, 'role' => 'admin'],
    ['name' => '鈴木花子', 'age' => 34, 'role' => 'editor'],
    ['name' => '佐藤次郎', 'age' => 22, 'role' => 'viewer'],
]);

// 巢狀陣列也可以
$products = collect([
    ['name' => '筆電', 'price' => 120000, 'stock' => 5],
    ['name' => '滑鼠', 'price' => 3500, 'stock' => 20],
    ['name' => '鍵盤', 'price' => 8000, 'stock' => 12],
]);
```

### `Collection::make()`

與 `collect()` 效果相同。若想以 Facade 風格書寫時可以使用。

```php theme={null}
$collection = Collection::make([1, 2, 3]);
```

### `Collection::fromJson()`

從 JSON 字串建立集合。處理外部 API 回應時很方便。

```php theme={null}
$collection = Collection::fromJson('[{"name":"山田太郎"},{"name":"鈴木花子"}]');
```

## 常用方法

### `map` — 轉換每個元素

對集合中的每個元素套用處理，回傳轉換後的新集合。

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'email' => 'yamada@example.com'],
    ['name' => '鈴木花子', 'email' => 'suzuki@example.com'],
]);

// 將電子郵件遮罩以供顯示
$masked = $users->map(function (array $user) {
    $parts = explode('@', $user['email']);
    return [
        'name' => $user['name'],
        'email' => substr($parts[0], 0, 2) . '***@' . $parts[1],
    ];
});

// [['name' => '山田太郎', 'email' => 'ya***@example.com'], ...]
```

### `filter` / `reject` — 依條件篩選

`filter()` 只保留符合條件的元素，`reject()` 則排除符合條件的元素。

```php theme={null}
$products = collect([
    ['name' => '筆電', 'price' => 120000, 'in_stock' => true],
    ['name' => '滑鼠', 'price' => 3500, 'in_stock' => false],
    ['name' => '鍵盤', 'price' => 8000, 'in_stock' => true],
]);

// 只取有庫存的商品
$inStock = $products->filter(fn ($product) => $product['in_stock']);

// 排除無庫存的商品（reject 是 filter 的反向）
$available = $products->reject(fn ($product) => ! $product['in_stock']);

// 沒有參數的 filter() 會濾掉 falsy 值
$names = collect(['山田', '', null, '鈴木', false])->filter()->values();
// ['山田', '鈴木']
```

<Tip>
  執行 `filter()` 之後索引會出現空缺。呼叫 `values()` 便可重新編為從 0 開始的連續索引。
</Tip>

### `first` / `last` — 取得一筆元素

回傳符合條件的第一個或最後一個元素。

```php theme={null}
$orders = collect([
    ['id' => 1, 'status' => 'shipped', 'amount' => 5000],
    ['id' => 2, 'status' => 'pending', 'amount' => 12000],
    ['id' => 3, 'status' => 'pending', 'amount' => 3500],
]);

// 取得第一筆未處理訂單
$nextOrder = $orders->first(fn ($order) => $order['status'] === 'pending');
// ['id' => 2, 'status' => 'pending', 'amount' => 12000]

// 若無符合條件時的預設值
$order = $orders->first(fn ($order) => $order['status'] === 'cancelled', null);

// 最後一個元素
$latest = $orders->last();
```

### `pluck` — 只取出特定 key 的值

從巢狀陣列或模型中僅擷取特定欄位。

```php theme={null}
$users = collect([
    ['id' => 1, 'name' => '山田太郎', 'department' => '開發部'],
    ['id' => 2, 'name' => '鈴木花子', 'department' => '業務部'],
    ['id' => 3, 'name' => '佐藤次郎', 'department' => '開發部'],
]);

// 只取出名字
$names = $users->pluck('name');
// ['山田太郎', '鈴木花子', '佐藤次郎']

// 用 id 作為 key 做映射
$nameById = $users->pluck('name', 'id');
// [1 => '山田太郎', 2 => '鈴木花子', 3 => '佐藤次郎']
```

### `groupBy` — 依特定 key 分組

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'department' => '開發部'],
    ['name' => '鈴木花子', 'department' => '業務部'],
    ['name' => '佐藤次郎', 'department' => '開發部'],
    ['name' => '田中美咲', 'department' => '業務部'],
]);

$byDepartment = $users->groupBy('department');
// [
//   '開發部' => [['name' => '山田太郎', ...], ['name' => '佐藤次郎', ...]],
//   '業務部' => [['name' => '鈴木花子', ...], ['name' => '田中美咲', ...]],
// ]

// 使用閉包動態指定分組 key
$byFirstChar = $users->groupBy(fn ($user) => mb_substr($user['name'], 0, 1));
```

### `sortBy` / `sortByDesc` — 排序

```php theme={null}
$products = collect([
    ['name' => '筆電', 'price' => 120000],
    ['name' => '滑鼠', 'price' => 3500],
    ['name' => '鍵盤', 'price' => 8000],
]);

// 依價格升序
$cheapFirst = $products->sortBy('price');

// 依價格降序
$expensiveFirst = $products->sortByDesc('price');

// 使用多個 key 排序
$sorted = $products->sortBy([
    ['price', 'asc'],
    ['name', 'asc'],
]);
```

### `each` — 對每個元素執行處理

用於帶有副作用的處理（日誌輸出、寄信等）。它本身不會修改集合。

```php theme={null}
$orders = collect([
    ['id' => 1, 'user_id' => 10, 'amount' => 5000],
    ['id' => 2, 'user_id' => 11, 'amount' => 12000],
]);

$orders->each(function (array $order) {
    \Log::info("處理訂單 #{$order['id']}", ['amount' => $order['amount']]);
    // 寄送通知或派發至佇列等
});

// 回傳 false 可中途跳出處理
$orders->each(function (array $order) {
    if ($order['amount'] > 10000) {
        return false; // 跳出迴圈
    }
    // ...
});
```

### `flatMap` — 映射後扁平化一層

將每個元素轉為陣列，並將整體扁平化為一維。

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'tags' => ['php', 'laravel']],
    ['name' => '鈴木花子', 'tags' => ['javascript', 'vue']],
]);

// 將所有使用者的 tag 攤平為單一清單
$allTags = $users->flatMap(fn ($user) => $user['tags']);
// ['php', 'laravel', 'javascript', 'vue']
```

### `reduce` — 匯總

將整個集合摺疊為單一值。

```php theme={null}
$orders = collect([
    ['product' => '筆電', 'quantity' => 1, 'price' => 120000],
    ['product' => '滑鼠', 'quantity' => 2, 'price' => 3500],
    ['product' => '鍵盤', 'quantity' => 1, 'price' => 8000],
]);

// 計算總金額
$total = $orders->reduce(
    fn ($carry, $order) => $carry + ($order['price'] * $order['quantity']),
    0
);
// 135000
```

<Tip>
  單純加總可以使用 `sum()`：`$orders->sum(fn ($o) => $o['price'] * $o['quantity'])`
</Tip>

### `reduceInto` — 匯總到物件中

與 `reduce` 類似進行摺疊處理，但差別在於 callback 不需回傳值。適合直接修改既有的物件。

```php theme={null}
class OrderStats
{
    public int $total = 0;
    public int $count = 0;
}

$orders = collect([
    ['amount' => 100],
    ['amount' => 250],
    ['amount' => 50],
]);

$stats = $orders->reduceInto(new OrderStats, function (OrderStats $stats, array $order) {
    $stats->total += $order['amount'];
    $stats->count++;
});

$stats->total; // 400
$stats->count; // 3
```

<Tip>
  `reduce` 的 callback 回傳值會成為下一次的 `$carry`，適合基本型別的匯總。`reduceInto` 則會持續修改同一個物件，程式碼可以更精簡。
</Tip>

若要匯總為純量或陣列，可在 callback 中使用參考傳遞（`&`）。

```php theme={null}
$collection = collect([1, 2, 3, 4, 5]);

$even = $collection->reduceInto([], function (array &$result, int $value) {
    if ($value % 2 === 0) {
        $result[] = $value;
    }
});

// [2, 4]
```

### `chunk` — 分割

將較大的集合切割為指定大小。適合批次處理或畫面顯示。

```php theme={null}
$users = collect(range(1, 100))->map(fn ($i) => ['id' => $i, 'name' => "使用者{$i}"]);

// 每 10 筆一組
$chunks = $users->chunk(10);
// $chunks->count() === 10

// 分批處理
$chunks->each(function (\Illuminate\Support\Collection $batch) {
    // 每批的 DB 操作或寄信等
});
```

## 方法鏈

集合真正的價值在於方法鏈。你可以串接多個操作，把複雜的資料轉換用一個表達式表達出來。

```php theme={null}
$orders = collect([
    ['customer' => '山田太郎', 'status' => 'completed', 'amount' => 5000, 'category' => '電子產品'],
    ['customer' => '鈴木花子', 'status' => 'pending',   'amount' => 12000, 'category' => '電子產品'],
    ['customer' => '佐藤次郎', 'status' => 'completed', 'amount' => 3500, 'category' => '文具'],
    ['customer' => '田中美咲', 'status' => 'completed', 'amount' => 8000, 'category' => '電子產品'],
    ['customer' => '伊藤健一', 'status' => 'cancelled', 'amount' => 2000, 'category' => '文具'],
]);

// 取得已完成的電子產品訂單，按金額降序排列，並僅擷取顧客名與金額
$result = $orders
    ->filter(fn ($order) => $order['status'] === 'completed')
    ->filter(fn ($order) => $order['category'] === '電子產品')
    ->sortByDesc('amount')
    ->map(fn ($order) => [
        'customer' => $order['customer'],
        'amount'   => number_format($order['amount']) . ' 元',
    ])
    ->values();

// [
//   ['customer' => '田中美咲', 'amount' => '8,000 元'],
//   ['customer' => '山田太郎', 'amount' => '5,000 元'],
// ]
```

## 與 Eloquent 結合

Eloquent 查詢的結果永遠是 `Illuminate\Database\Eloquent\Collection` 實例。
它繼承了基本的 `Collection`，因此可以完整使用上述方法。

```php theme={null}
use App\Models\User;
use App\Models\Order;

// get() 的結果是集合
$users = User::where('is_active', true)->get(); // Collection

// 可直接使用集合方法
$adminEmails = User::all()
    ->filter(fn ($user) => $user->role === 'admin')
    ->pluck('email');

// 已載入關聯的資料也可以用集合操作
$orders = Order::with('items')->where('status', 'completed')->get();

$summary = $orders->map(fn ($order) => [
    'id'         => $order->id,
    'customer'   => $order->user->name,
    'item_count' => $order->items->count(),
    'total'      => $order->items->sum('price'),
]);
```

<Warning>
  資料庫可以處理的篩選或排序，請透過查詢建構器（`where`、`orderBy` 等）進行，而非集合操作。若在轉為集合之後才篩選，將把不必要的資料整批讀入記憶體。
</Warning>

### Eloquent 專屬的集合方法

`Eloquent\Collection` 提供額外的方法。

```php theme={null}
$users = User::all();

// 依 ID 尋找模型
$user = $users->find(1);

// 取得主鍵集合
$ids = $users->modelKeys(); // [1, 2, 3, ...]

// 一併載入關聯
$users->load('orders', 'profile');

// 差集、交集
$diff = $users->diff($otherUsers);
$intersect = $users->intersect($otherUsers);
```

## Lazy Collection

一般的集合會把全部資料讀入記憶體，而 `LazyCollection` 利用 PHP 的 generator，逐筆處理資料。
處理數萬筆以上的大量資料時可節省記憶體。

```php theme={null}
use Illuminate\Support\LazyCollection;

// 一般集合：把全部資料讀入記憶體
$users = User::all(); // 10 萬筆就會有 10 萬筆同時在記憶體

// LazyCollection：逐筆處理
User::cursor()->each(function (User $user) {
    // 逐筆處理使用者
    // 每次只需在記憶體保留一筆
});
```

### 建立 LazyCollection

```php theme={null}
use Illuminate\Support\LazyCollection;

// 從閉包（generator）建立
$lazy = LazyCollection::make(function () {
    $handle = fopen('large-file.csv', 'r');

    while (($line = fgetcsv($handle)) !== false) {
        yield $line;
    }

    fclose($handle);
});

// Eloquent 的 cursor() 方法會回傳 LazyCollection
$lazy = User::where('is_active', true)->cursor();
```

### 大量資料的批次處理

```php theme={null}
use App\Models\Order;

// 逐筆處理所有訂單（記憶體效率佳）
Order::cursor()
    ->filter(fn ($order) => $order->amount > 10000)
    ->each(fn ($order) => $order->sendConfirmationEmail());

// 用 cursor() 逐筆抓取，並以 filter/each 建立處理流水線
Order::where('status', 'completed')
    ->cursor()
    ->filter(fn ($order) => $order->amount > 10000)
    ->each(fn ($order) => $order->sendConfirmationEmail());
```

<Info>
  `cursor()` 會從資料庫逐筆抓取，處理大量紀錄時能大幅節省記憶體。但資料庫連線會維持到處理完成為止。
</Info>

### 使用 `take` 與 `skip` 進行分頁

```php theme={null}
$lazy = LazyCollection::make(function () {
    foreach (range(1, 1000000) as $i) {
        yield $i;
    }
});

// 跳過前 1000 筆，取得下 100 筆
$page = $lazy->skip(1000)->take(100)->values();
```

## 總結

<AccordionGroup>
  <Accordion title="常用方法整理">
    | 方法                            | 說明             |
    | ----------------------------- | -------------- |
    | `collect($array)`             | 建立集合           |
    | `map($callback)`              | 轉換每個元素         |
    | `filter($callback)`           | 依條件篩選          |
    | `reject($callback)`           | 排除符合條件的元素      |
    | `first($callback)`            | 取得符合條件的第一個元素   |
    | `pluck($key)`                 | 擷取特定 key 的值    |
    | `groupBy($key)`               | 依特定 key 分組     |
    | `sortBy($key)`                | 升序排序           |
    | `sortByDesc($key)`            | 降序排序           |
    | `each($callback)`             | 對每個元素執行處理（副作用） |
    | `flatMap($callback)`          | 映射後扁平化         |
    | `reduce($callback, $initial)` | 匯總為單一值         |
    | `chunk($size)`                | 依指定大小分割        |
    | `values()`                    | 重新編排索引         |
    | `sum($key)`                   | 計算加總           |
    | `count()`                     | 取得筆數           |
  </Accordion>

  <Accordion title="集合 vs 陣列函式">
    集合比陣列函式擁有更高的可讀性。

    ```php theme={null}
    // 使用陣列函式
    $result = array_values(array_filter(
        array_map(fn ($u) => $u['name'], $users),
        fn ($name) => strlen($name) > 2
    ));

    // 使用集合
    $result = collect($users)
        ->pluck('name')
        ->filter(fn ($name) => strlen($name) > 2)
        ->values()
        ->all();
    ```
  </Accordion>

  <Accordion title="一般集合 vs LazyCollection">
    * **一般集合**：數百至數千筆資料。單純且直覺。
    * **LazyCollection**：數萬筆以上大量資料，需節省記憶體時使用。搭配 `cursor()` 效果最佳。
    * Eloquent 的 `get()` 回傳一般集合，`cursor()` 回傳 LazyCollection。
  </Accordion>
</AccordionGroup>


## Related topics

- [Eloquent 集合](/zh-TW/eloquent-collections.md)
- [輔助函式](/zh-TW/helpers.md)
- [Eloquent API 資源](/zh-TW/eloquent-resources.md)
- [Laravel Scout](/zh-TW/scout.md)
- [HTTP 測試](/zh-TW/http-tests.md)
