【Laravel】Storage機能で保存したCSVをExcelで文字化けさせない方法

Webアプリケーションの案件には、結果をCSVで出力したいと言う要望はよくあります。
その要望につきまとうのが、Excelでみたときに文字化けする問題です。

さて、
今回は、この問題の解決方法について触れます。
また、ファイルの出力はLaravelのStorage機能を使用した場合を想定した解説です。

イントロダクション

この記事で得られる事

  • PHPで保存するファイルに、BOMファイルを付与する方法がわかる。
  • Laravelの場合の対応方法がわかる。

なぜ文字化けするのか

ExcelのデフォルトエンコードはShift-JISだから文字化けを起こします。
そのため、Excel側で読み込む際に文字コードを変えてあげたり、CSVをテキストエディタで開いて文字コードを変えてあげれば正しく表示出来ます。
しかし、その作業は手間と考える人が多く、それはバグだと思われるケースの方が圧倒的に多いです。

もちろん、最初からShift-JISを使う、もしくは、出力時にUTF-8からShift-JISに変換すると言う方法もあります。
しかし、UTF-8の方がサポートしている文字は多いです。
直近で問題が無くても、運用していいくうちに恐らく何処かで問題が生じます。

ExcelでUTF-8のファイルを文字化けせずに開く方法

ExcelでUTF-8と認識させる方法として、BOM付きUTF-8で保存すると言う方法があります。
この形式で保存されたデータであれば、ExcelでおUTF-8だと認識してくれます。

基本

ファイルのheader情報で指定する方法もあります。
手っ取り早い方法は、ファイルに書き出すときに先頭に追加してあげます。

file_put_contents(<UTF-8のファイル>, "\xEF\xBB\xBF".  $content);

この際、BOMのコードは必ずダブルコーテーションで指定してください。

本題

応用してStorageに保存する方法に解説します。
次のコードが、配列をCSVの形式に変換してストレージに保存する処理です。

use Storage;
use League\Csv\Writer;

class makeCsv
{
    function publishStorage() {

        $header = [
            'code', 'name', 'address'
        ];

        $record = [
            [
                'code' => 1,
                'name' => '太郎',
                'address' => '東京都',
            ],
            [
                'code' => 2,
                'name' => '二郎',
                'address' => '京都府',
            ],
            [
                'code' => 3,
                'name' => '三郎',
                'address' => '北海道',
            ]
        ];

        $csv = Writer::createFromString(new \SplTempFileObject());
        $csv->insertOne($header);
        $csv->insertAll($records);

        Storage::disk(<disk名>)->put(<出力先>, "\xEF\xBB\xBF" . $csv->__toString());

    }
}

以上です。
尚、今回作成するテストコードでは、CSVのコンバート用にLeague\Csvを使用しています。
もし、このコードを参考にされる場合は、ライブラリの設定をお願いします。

csv.thephpleague.com

解説

Storageに保存する処理は次の部分です。 出力するためのputの引数に、BOM付きの文字を追加します。

Storage::disk(<disk名>)->put(<出力先>, "\xEF\xBB\xBF" . $csv->__toString());

実際にputはどういった処理をしているかというと下記です。

public function put($path, $contents, $lock = false)
    {
        return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
    }

そのため、file_put_contentsと同じ要領で指定すれば対応できます。