はじめに
CSV を Laravel で出力しようと思ったら、関数一発かませば終わり、とはいきません。
以前利用していた FuelPHP はその辺楽だったんですが、Laravel では横着ができないので
もうちょっとビシッと書く必要があります。
意外と知られてないけど
だいたい CSV って Excel で読ませますよね?
UTF-8 で出力すると Excel で読んだ時に化けるからみなさん一生懸命
Shift-JIS化してると思います。私もそうでした。
ですが BOM をつけてやると UTF-8 のまま出力しても Excel で読めます。
まあ、なんて便利なのでしょう...
間違った書き方
public function csv()
{
// リスト
$lists = [
['おはよう', 'おやすみ'],
['こんにちは', 'さようなら'],
];
// ファイル生成
$stream = fopen('php://output', 'w');
fwrite($stream, pack('C*',0xEF,0xBB,0xBF)); // BOM をつける
// ヘッダー
fputcsv($stream, ['header1', 'header2']);
//
foreach ($lists as $list) {
fputcsv($stream, $list);
}
return response(stream_get_contents($stream), 200)
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename="demo.csv"');
}
何が問題か
これは $header
や $list
のデータサイズが小さければ全く問題なく出力されるんですが、
一行がある程度の長さを越えると CSV ではなく、レスポンスがなぜか0バイトになったり HTTP ヘッダの
Content-Type が text/html になったりします。
php://output
の部分を php://temp
や php://memory
にしても解決しませんでした。
output だとヘッダが代わり、後者の二つだと0バイトという現象でした。
カラム内のデータサイズにもよるんですが、体感では50-80カラムくらい横に伸ばすと
NGになってしまいました。
正しい書き方
自分はまずトレイトを作りました。
namespace App\Http\Traits;
trait Csv {
/**
* CSVファイルを生成する
* @param $filename
*/
public static function createCsv($filename) {
$csv_file_path = storage_path('app/'.$filename);
$result = fopen($csv_file_path, 'w');
if ($result === FALSE) {
throw new Exception('ファイルの書き込みに失敗しました。');
} else {
fwrite($result, pack('C*',0xEF,0xBB,0xBF)); // BOM をつける
}
fclose($result);
return $csv_file_path;
}
/**
* CSVファイルに書き出す
* @param $filepath
* @param $records
*/
public static function write($filepath, $records) {
$result = fopen($filepath, 'a');
// ファイルに書き出し
fputcsv($result, $records);
fclose($result);
}
/**
* CSVファイルの削除
* @param $filename
*/
public static function purge($filename) {
return unlink(storage_path('app/'.$filename));
}
}
次にコントローラーで利用できるようにします。
use App\Http\Traits\Csv;
class DemoController extends Controller
{
use Csv;
// 略
}
で、method 側はこう
public function csv() {
// リスト
$lists = [
['おはよう', 'おやすみ'],
['こんにちは', 'さようなら'],
];
$filename = 'demo.csv';
$file = Csv::createCsv($filename);
// ヘッダー
Csv::write($file, ['header1', 'header2']);
// 値を入れる
foreach ($lists as $list) {
Csv::write($file, $list);
}
$response = file_get_contents($file);
// ストリームに入れたら実ファイルは削除
Csv::purge($filename);
return response($response, 200)
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename='.$filename);
}
というわけで、stream に全部いれて横着しようとしたらあかんかった、という結論です。
php.ini のパラメータどっかいじればいいんでしょうけど、まあ実際はファイルにちょびちょび書いていったらいいだけでした。
よく考えたら
Laravel感があるのストレージパスとかだけやった。