Laravel で API リソースを使って CSV ダウンロードする

## はじめに

Laravel で CSV ダウンロードするときのアーキテクチャはいくつかある。

- コントローラに処理を書く
- [ファサードを使う](https://qiita.com/niiyz/items/83770cfa6d6bb33c10ab)
- [サービスを使う](https://mintaku-blog.net/laravel-csv/) 
- [ライブラリを使う](https://github.com/usmanhalalit/laracsv)

けど、コントローラからモデルをとって、整形して、CSV にするなら、
[標準の JSON API リソース](https://laravel.com/docs/5.8/eloquent-resources)が JSON じゃなくて CSV になるだけでもよいのではないかと思った。
そして意外となかったので、書いてみます。


## CSV API リソースを実装する

[標準の JSON API リソースの実装](https://github.com/laravel/framework/tree/5.8/src/Illuminate/Http/Resources/Json)を見て、最低限な次の機能を実装しました。

- CSV だと複数のレコードを扱うように思うので、コレクションを扱う `ResourceCollection` のみ
- 整形とダウンロードのレスポンス生成のみ（ほかにも機能があるっぽい）

```php:ResourceCollection.php
<?php

namespace App\Http\Resources\Csv;

use Illuminate\Contracts\Support\Responsable;

abstract class ResourceCollection implements Responsable
{
    protected $collection;

    public function __construct($resource)
    {
        $this->collection = $resource;
    }

    public function toResponse($request)
    {
        return response()->streamDownload(function () {
            $file = new \SplFileObject('php://output', 'wb');

            $header = $this->attributes();
            if (! empty($header)) {
                $file->fputcsv($this->convertCharset($header));
            }

            foreach ($this->toArray() as $fields) {
                $file->fputcsv($this->convertCharset($fields));
            }
        }, $this->fileName());
    }

    private function convertCharset($fields)
    {
        return collect($fields)
            ->map(function ($item) {
                return mb_convert_encoding($item, 'SJIS-win', mb_internal_encoding());
            })
            ->all();
    }

    /**
     *  見出しがあれば返します
     */
    protected function attributes()
    {
        return [];
    }

    /**
     *  整形した配列を返します
     */
    abstract protected function toArray();

    /**
     *  ダウンロードファイルの名前があれば返します
     */
    protected function fileName()
    {
        return 'download.csv';
    }
}
```


## 使ってみる

注文情報を CSV に書き出すことを考えてみましょう。

### コントローラ

ほとんど JSON のときと同じです。

```php:OrderController.php
<?php

namespace App\Http\Controllers;

use App\Order;
use App\Http\Resources\ReportResource;

class OrderController extends Controller
{
    public function report()
    {
        return new ReportResource(
            Order::query()
                ->where('is_canceled', false)
                ->get()
        );
    }
}
```

### API リソース

JSON のときと同じく、`toArray` をオーバライドして整形します。
加えて、`attributes` で見出しを、`fileName` でダウンロードファイル名を、オーバライドして指定することもできます。

```php:ReportResource.php
<?php

namespace App\Http\Resources;

use App\Http\Resources\Csv\ResourceCollection;

class ReportResource extends ResourceCollection
{
    protected function toArray()
    {
        $this->collection->load(['user', 'products']);

        return $this->collection
            ->map(function ($order) {
                return [
                    $order->id,
                    $order->user->id,
                    $order->user->name,
                    $order->products->sum('price'),
                ];
            })
            ->all();
    }

    protected function attributes()
    {
        return [
            '注文ID',
            '会員ID',
            '会員名',
            '注文金額合計',
        ];
    }

    protected function fileName()
    {
        return '注文一覧.csv';
    }
}
```

### ダウンロード例

```csv:注文一覧.csv
注文ID,会員ID,会員名,注文金額合計
1,1,ほげほげ,1000
2,1,ほげほげ,1500
3,2,ふがふが,1000
4,2,ふがふが,2000
```


## おわりに

JSON のときと同じ使い方ができるので、覚えることが少ないし、標準にも近いかもしれない。

すっきり！