LaravelでCSVファイルをエクスポートする

### やりたいこと
**Laravelを使って、テーブルから取得したデータをCSVファイルとしてダウンロードする**

CSVファイルのエクスポート（アップロード）については別の記事にまとめたのでそちらを参照ください。
https://qiita.com/gohandesuyo/items/18ad77bad0b7e068d6f7

ここではテーブルを用意して、テーブルに格納されたレコードを取得する実装例とします。サンプルとなるテーブルの定義は以下の通りとします。
DBはMySQLを前提としています。

|カラム|型|PK|not null|unique|
|:--:|:--:|:--:|:--:|:--:|
|id|int|〇|〇|〇|
|login_id|varchar(20)||〇|〇|
|password|varchar(255)||〇||
|name|varchar(255)||〇||
|email|varchar(255)||〇||
|created_at|timestamp||〇||
|updated_at|timestamp||〇||

### 実装方法
CSVエクスポートの実装方法は大きく分けて２つ。

1. バックエンドCSVファイルを作成してファイルのパス（リンク）をレスポンスとして返す
* CSVの中身をレスポンスとして返し、フロント側でCSVファイルを作成する

ここでは2の方法を採用します。
また、1でも2でもCSVの中身を作成する方法も色々なやり方があるかと思います。
個人的にループ回しながら処理するのはあまり好きじゃないので、DBからデータを取得する時点でカンマ区切りでデータが並んでいる状態にして、それをそのままレスポンスとして返すような実装にしたいと思います。

### サービス
テーブルからデータを取得してCSV形式のデータを文字列を返す。

```UserCsvService.php
<?php
namespace App\Services;

use Exception;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class UserCsvService {

    const CSV_EXPORT_HEADER = [
        "id",
        "login_id",
        "password",
        "name",
        "email",
        "created_at",
        "updated_at",
    ];

    public function userExport()
    {
        // ファイルヘッダーとなる文字列作成
        $header = collect(self::CSV_EXPORT_HEADER)->implode(",");
        // select句になる文字列作成
        $selectStr = collect(self::CSV_EXPORT_HEADER)->map(function($item) {
            return "ifnull({$item}, '')";
        })->implode(", ',' ,");
        // データの取得
        $users = DB::table('users')
        ->select(DB::raw("concat({$selectStr}) record"))
        ->pluck("record");
        // ヘッダーとデータを加えて改行コードでつなげて１つの文字列にする
        return $users->prepend($header)->implode("\r\n");
    }
}


```

### コントローラ
サービスのメソッドを呼び出して結果をレスポンスとして返す。

```CsvController.php
<?php

namespace App\Http\Controllers;

use App\Services\UserCsvService;

class CsvController extends Controller
{
    protected $service;

    public function __construct(UserCsvService$service)
    {
        $this->service = $service;
    }

    public function userExport()
    {
        $data = $this->service->userExport();
        return response($data , 200);
    }
}
```

```api.php
<?php

use App\Http\Controllers\CsvController;

// 中略

Route::post('userExport', [CsvController::class, 'userExport']);
```

### 画面
画面はテキトーです。最低限の実装のみにしてます。
文字化け対策とか考慮してないのでそのあたりは別の記事を参考に。
index.blade.php

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>CSV-Export-Sample</title>
</head>
<body>
    <button id="download">CSVダウンロード</button>
    <script>
        'use strict'

        document.getElementById('download')
        .addEventListener('click', () => {
            fetch('/api/userExport', {
                method: 'POST',
                headers: {
                    'X-CSRF-TOKEN': document.getElementsByName('csrf-token')[0].content,
                    'Content-Type': 'application/json'
                    },
            })
            .then(response => response.blob().then(blob => {
                let link = document.createElement('a')
                link.href = window.URL.createObjectURL(blob)
                link.download = 'users.csv'
                link.click()
            }))
        })
    </script>
</body>
</html>
```

### ポイント
ポイントはサービスでの処理でしょうか。コレクションのメソッドを駆使してSQLのselect句を組み立てていますが、ぱっと見ではどんなSQLになるかイメージしにくいかもしれません。実行されるSQLは以下のようになります。

```sql
select concat(
    ifnull(id, ''), ',' 
    ,ifnull(login_id, ''), ',' 
    ,ifnull(password, ''), ',' 
    ,ifnull(name, ''), ',' 
    ,ifnull(email, ''), ',' 
    ,ifnull(created_at, ''), ',' 
    ,ifnull(updated_at, '')
    ) record
from users;
```
やってることは全てのカラムをカンマをつけながら一つの文字列に結合しています。
MySQLの場合は文字列結合にconcat関数を使うので上記のようになります。
そして値にnullがあると結果がnullになってしまうので、ifnull関数で被せてnullの場合は空白を設定しています。
他のDBの場合は文字列結合が || や + 演算子だったりするので、MySQL以外の場合はDBに合わせて適当に修正してください。

### まとめ
この実装ではDBMSの種類によってselect句の実装が変わるのが少しネックです。
ですが１行ずつループする処理がないので私のようにループ処理を書くのが嫌いな人にはお勧めの実装方法かと。