LaravelでCSVファイルのダウンロード機能を実装する

Laravelでの開発中、DBのデータが記載されたCSVファイルのダウンロード機能を実装する必要があり学習したのでまとめる。
ただし最後の1行だけ理解が及んでおらずのため悪しからず。。。

# そもそもCSVとは？
<b>「Comma Separated Value」</b>の略。
直訳すると、<b>カンマで区切った値</b>となる。

散々CSVファイルを見てきたがカンマなど見たことがない、、、
どこがComma Separated Valueなんだと思ってしまった。

まず大前提として自分の認識が間違っていたのが、CSVとExcelはほぼ同じだと思っていたことだ。
というかむしろ何が違うんだくらいの認識だった。

ただこれこそが最大の勘違いで、正確には普段目にしている<b>CSVファイルはExcelで開かれていた</b>ため全く同じに見えていたということだ。
となるとCSVファイルの実体は何なのか。
試しにCSVファイルをエディターに投げてみると以下のように表示される。

```
id,name\n  
1,"taro"\n  
2,"mike"\n  
3,"john"\n
```

まさにカンマで区切られた値、すなわちComma Separated Valueなのである。

<b>WindowsではCSVファイルは既定でExcelに関連付けられている</b>ので、CSVファイルをダブルクリックするとExcelが起動する。
そのためCSV＝Excelのように思えてしまうのだ。

# CSVダウンロード実装
ここから本題に入っていく。
大まかな流れとしては以下となる。

①書き込み用のファイルを開く
②ファイルにデータを書き込む
③書き込んだデータを文字列に変換・エンコードする
④ファイルを閉じる
⑤ファイルをCSVファイルに変換する

# ①書き込み用のファイルを開く
まずは空のファイルを作成し、データを書き込めるよう開いておく必要がある。
その際に使うのが`fopen関数`である。

## fopen関数
ファイルまたはURLをオープンする関数。
あるファイルの情報を新しいファイルに書き込む、DBのデータを取得してファイルに書き込むといった際、いずれも<b>どのファイルに何をしたいのか</b>を示す必要がある。

そこでまずは`fopen関数`を使って<b>対象のファイルとアクション</b>（`書き込み用`、`読み込み用`、`追記用`）を指定してファイルを準備する。
※書き込みと追記の違いは、ファイルの先頭から書き込むか終端から書き込むか

形式としては以下のような形となり、<b>第一引数に開くファイル名、第二引数に開くモードを指定する。</b>

```php
$stream = fopen('php://temp', 'w');   // php://tempについては後述
```

モードについては以下参照。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131391/297625ac-cbda-6cca-455a-13c7c8d671ff.png)
そして上記の場合、変数`$stream`には<b>ファイルポインタ</b>というものが格納される。

## ファイルポインタ
テキストエディタのカーソルのようなもので、ファイルに対するアクションをどこから始めるのかを指定したもの。
これを動かして処理の開始地点を変更することもできる。

## CSVファイルを作成する際にどのファイルを使用するか
上記の例で`php://temp`という見慣れないファイルを使用しているがこれは何なのか。

>php://memory および php://temp は読み書き可能なストリームで、一時データをファイルのように保存できるラッパーです。
両者の唯一の違いは、php://memory が常にデータをメモリに格納するのに対して php://temp は定義済みの上限 (デフォルトは 2 MB) に達するとテンポラリファイルを使うという点です。
https://www.php.net/manual/ja/wrappers.php.php

とある。
極論、CSVファイルがダウンロードされるたびに新しいファイルを作成することもできるが、毎回新しいファイルが作成されてしまうのは得策とは思えない。
そこで、<b>ストリーム</b>に一時的に保存して対応しているのである。

## ストリーム
> プログラミングの分野では、データの入出力全般を扱う抽象的なオブジェクトやデータ型を意味する場合が多い。データが出入りする何らかの対象（メモリ領域やファイル、ネットワークなど）をプログラム中で扱えるように抽象化したもので、接続や切断、書き込みや読み込みなどを簡易な操作で行うことができる。
https://e-words.jp/w/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0.html

いまいちピンとこないとは思うが、要は流動的なデータを一時的に留めておく場所だと思って良いかと思う。
<b>テンポラリファイル</b>も同じようなもので、一時的に作られるファイルのことを指す。
`tempファイル`、`tmpファイル`のように記載されることが多い。


# ②ファイルにデータを書き込む
`fputcsv関数`を使用する。
<b>第一引数に</b>`fopen関数`<b>で開いたファイルポインタ、第二引数に書き込みたい内容を指定する。</b>
記述例としては以下。

```php
$users = User::all();
$stream = fopen('php://temp', 'w');   //書き込みモードで開く
$arr = array('id', 'name');           //CSVファイルのカラム（列）名の指定
fputcsv($stream, $arr);               //1行目にカラム（列）名のみを書き込む（繰り返し処理には入れない）
foreach ($users as $user) {
    $arrInfo = array(
        'id' => $user->id,
        'name' => $user->name
        );
    fputcsv($stream, $arrInfo);       //DBの値を繰り返し書き込む
}
```

上記の場合、`php://temp`の1行目にまずCSVファイルのカラム名を記載し、2行目以降にDBの内容を書き込んでいる。
また、ここで条件分岐させてデータがない場合は「該当データがありません。」などの記載をするのも一つのやり方だ。

# ③書き込んだデータを文字列に変換・エンコードする
ストリームに書き込んだデータは文字列ではないので、文字列に変換してあげる必要がある。
実際、`fputcsv関数`で書き込まれたストリームを`var_dump`で確認してみると、`resource(358) of type (stream)`と表示される。
これはリソース型というらしい。
>リソース型 は PHPの外部世界 とやり取りを行うデータが格納されている型
リソース はデータベースや画像ファイルなどの 外部情報
リソース型 の種類は 特殊型
リソース型 のチェックは is_resource() 関数 で行う
https://wepicks.net/resourcetype/

このままではCSVファイルに変換ができないため、文字列に変換してあげる。
ここで使用するのが`stream_get_contents関数`と`mb_convert_encoding関数`。
`fputcsv関数`で書き込まれたストリームを`stream_get_contents関数`で取得し文字列に変換後、`mb_convert_encoding関数`でエンコードするという流れになる。

## stream_get_contents
`stream_get_contents関数`とは、
> file_get_contents() と似ていますが、 stream_get_contents() は既にオープンしている ストリームリソースに対して操作を行います。そして、指定した offset から始まる最大 maxlength バイトのデータを取得して文字列に保存します。
https://www.php.net/manual/ja/function.stream-get-contents.php

とのこと。
`php://temp`は前段で記載の通りストリームであるためこの関数を使用する。
書き方と具体的な説明は以下。

```php
stream_get_contents(resource $handle, int $maxlength = -1, int $offset = -1)
```
<b>第一引数</b>：`fopen関数`で開いたストリーム（これさえ指定すれば第２・第３引数を指定せず使用可能）
<b>第二引数</b>：読み込むバイト数（指定しなければデフォルト値の-1が適用され、残りのバッファ全てを読み込む）
<b>第三引数</b>：読み込みを開始する前に移動する位置。負の数を指定した場合は移動が発生せず、現在位置から読み込みを開始する。

第一引数の説明は良いとして、第二引数と第三引数について見ていく。
第二引数の読み込むバイト数を指定するタイミングがあまり想像できなかったので、特に指定しなくても問題ないと思う。

では第三引数について。
今回であればストリームに書き込んだデータのどこからを文字列に変換したいかということだ。

ここで一つ考えなくてはならないのが、`fputcsv関数`で書き込まれた後のファイルポインタがどこになるのかということだ。
結論としては、<b>ファイルの終端</b>となる。

そのため、第三引数を指定しないと以降の処理がファイルの終端から進んでしまう。
そして終端以降は当然何も書き込まれていないため何も起こらない。

そこで、`stream_get_contents関数`の第三引数を指定し、ファイルポインタを先頭（0地点）に戻す必要がある。
以下のようにしてやる。

```php
$csv = stream_get_contents($stream, -1, 0);   //第三引数を指定するために第二引数も合わせて指定
```

また、別のやり方として`rewind関数`を使用する方法もある。
`rewind関数`はファイルポインタを先頭に戻してくれるため、`stream_get_contents関数`の前に記載することで、ストリームの最初から最後までを取得することができる。
以下のようにするだけ。

```php
rewind($stream);
$csv = stream_get_contents($stream);   //ファイルポインタの変更が必要なくなったので第一引数のみで問題なし
```
## エンコーディング
ここまででストリームの取得ができたので、後は`mb_convert_encoding関数`でエンコーディングしてあげるのみ。
今回の実装では以下のようにした。

```php
$csv = mb_convert_encoding($csv, 'sjis-win', 'UTF-8');
```
<b>第一引数が対象文字列、第二引数が変更後の文字エンコード、第三引数が変更前の文字エンコード</b>だ。
ここでは、`$csv`の文字エンコードを`UTF-8`から`sjis-win`に変更している。

CSVファイルをExcelが開く際には強制的に`sjis-win`で開くのだが、PHPスクリプトは`UTF-8`で書かれているため文字化けしてしまう。
これを防ぐためにエンコーディングをしている。

# ④ファイルを閉じる
ここまでの処理が完了すれば追加でファイルに手を加えることはないため、`fclose関数`を使用してファイルを閉じる。
引数にストリームを指定するのみだ。

```php
fclose($stream);
```

# ⑤ファイルをCSVに変換する
最後にファイルをCSVに変換しブラウザに返して終了となる。
まずはCSVに変換するためにヘッダーに持たせたい情報を指定する。

```php
$headers = array(
    'Content-Type' => 'text/csv',
    'Content-Disposition' => 'attachment; filename=test.csv'
);
```
## Content-Type
`Content-Type`とは、そのファイルがどんな種類のものなのかを示すもの。
「タイプ/サブタイプ」という記載の仕方で、「タイプ」でデータの種類（テキスト、画像、動画など）を定義し、「サブタイプ」で具体的なデータ形式を定義している。
text/plain, text/html, application/jsonのようになる。

## Content-Disposition
`Content-Disposition`は、コンテンツをwebページの一部として表示するかダウンロードするかを指定する。
`inline`を指定すればwebページとして、`attachment`を指定すればダウンロードファイルとして表示する。
また、`filename`でファイルの初期名を指定できる。

## ブラウザにCSVファイルを返す
ここまでできたら、最後はブラウザにCSVファイルを返すのみ。
以下のようにする。

```php
return Response::make($csv, 200, $headers);   //$csvは最後にmb_convert_encoding関数でエンコードした変数
```

この最後の1行が理解できておらず、、、
他にも色々書き方があるみたいだが、とにかくこのようにすると生成したCSVファイルをダウンロードできる。

改めてコード全体を見ると以下のようになる。

```php
$users = User::all();
$stream = fopen('php://temp', 'w');   //ストリームを書き込みモードで開く
$arr = array('id', 'name');           //CSVファイルのカラム（列）名の指定

fputcsv($stream, $arr);               //1行目にカラム（列）名のみを書き込む（繰り返し処理には入れない）
foreach ($users as $user) {
    $arrInfo = array(
        'id' => $user->id,
        'name' => $user->name
        );
    fputcsv($stream, $arrInfo);       //DBの値を繰り返し書き込む
}

rewind($stream);                      //ファイルポインタを先頭に戻す
$csv = stream_get_contents($stream);  //ストリームを変数に格納
$csv = mb_convert_encoding($csv, 'sjis-win', 'UTF-8');   //文字コードを変換

fclose($stream);                      //ストリームを閉じる

$headers = array(                     //ヘッダー情報を指定する
    'Content-Type' => 'text/csv',
    'Content-Disposition' => 'attachment; filename=test.csv'
);

return Response::make($csv, 200, $headers);   //ファイルをダウンロードする
```

以上で完了となる。
今回はCSVファイルに絞って記載したが、他のファイルでも同じように実装することができる。
実際、icsファイルのダウンロード機能を実装した際にも同じ要領で進めることができた。
次回はicsファイルやカレンダー追加機能の実装について書きたいと思う。