はじめに
仕事で Laravel 内でcsv を生成するコードを作成したのでその上で知ったことに関して備忘録代わりにこちらで記載しておきます。
保存場所の検討
Laravel のコードは当然ながら PHP で動いているので、ファイルを生成するのであれば、fopen((パス), (オプション))
を用いることになります。
(fopen から fclose までの一般的なファイル生成手法に関してはこの記事では割愛しますので、気になる方は各自お調べください)
この時、
response()->streamDownload($callback, $filename, $header)
のようなファイルを生成したそばからレスポンスとして返すような場合は、$callback に渡すクロージャ内でfopen('php://output', 'w')
と
ファイルのパスを設定すれば問題ありません。
しかし、生成を行って一度保存する場合、その保存先を用意するにあたって気を付けるべき項目が存在します。
例えば、 /storage/app/csv/{users.id} といったディレクトリに保存しようとした場合、すぐに
$path = storage_path() . '/app/csv/' . $user->id . '/' . $filename;
$stream = fopen($path, 'w');
といった形でストリームを開きたくなりますが、実際のディレクトリの状況を考慮しないと、該当するディレクトリが存在しない、権限が違って開けないなどの問題が生じます。
そのため、気を付けるべきことの一点目は 先にディレクトリを作成しておく です。
ディレクトリの作成(その1)
実際のディレクトリ生成の手段としては、2通り存在し、1つ目は mkdir() を用いるパターンです。
mkdir() は php 側のメソッドで、指定されたディレクトリを作成するメソッドです。
このメソッドは以下の2点に気を付ける必要があります。
1:存在しないディレクトリに対してディレクトリを作ろうとすると warning が発生する
2:既に指定のディレクトリが存在する場合も warning が発生する
上記を考慮する関係で、ファイルやディレクトリの存在を確認できる file_exists() を用いて以下の様な実装を行う必要が存在します。
$storagePath = storage_path();
if(!file_exists($storagePath . '/app/csv')) {
mkdir($storagePath . '/app/csv');
}
if(!file_exists($storagePath . '/app/csv/' . $user->id)) {
mkdir($storagePath . '/app/csv/' . $user->id);
}
上記のコードを見てわかる通り、存在が不確かなディレクトリを上から1つ1つ存在を確認し、生成していく形になります。
ディレクトリの作成(その2)
2つ目ファイル生成の手法は Storage::makeDirectory() を用いるパターンです。
Storage::makeDirectory() は Laravel 側のファサードに紐づくメソッドで、指定されたディレクトリを作成するメソッドです。
このメソッドは mkdir() と違って途中のディレクトリが存在しない場合や既に存在するディレクトリを指定した場合も問題なく実行できます。
しかし、Storage::makeDirectory() が Laravel 側のメソッドである関係上、php や laravel の設定次第では、生成されたディレクトリの作成者が mkdir() とは異なることがあります。この場合、fopen() でファイル作成が行われる関係上、ファイルの作成ができないといったパーミッションエラーが発生することがあります。
もしも Storage::makeDirectory() でディレクトリを生成したのちにファイルの生成ができないパーミッションエラーが発生した場合、以下の様な対応で解決できるかと思います。
・ディレクトリの権限を書き換える
・Storage::makeDirectory() での作成者が同じになるように設定を調整する
・Storage::makeDirectory() をやめて fopen() と同じ php 側の mkdir() を使う
文字コードの検討
ここまではディレクトリの検討に関して記載してきましたが、続いて CSV として検討すべき事項としては文字コードの問題があります。
基本的に、特に指定をせずにfwrite()
やfputcsv()
を行うと、 UTF-8 で記載されます。
しかし、今回の対象が CSV であることを前提にすると、windows 環境では excel で扱われることが容易に想像でき、単なる UTF-8 の CSV を excel で開くと、2バイト文字は見事に文字化けしてしまいます。
そのため、excel でも対応できるような対策を講じる必要があるわけですが、対策としては主に2つの方法が存在します。
文字コードの対応(その1)
1つ目の対応方法は、excel が想定している文字コードである Shift-JIS に変換する方法です。
PHP において文字コードは mb_convert_variables()
で変数単位で変換ができ、以下の様に扱います。
$csv = ['ID', '名前', '作成日時'];
mb_convert_variables('SJIS-win', 'UTF-8', $csv);
mb_convert_variables() を扱う注意点として、第3引数には変数のみが入れられるので、配列等の値を変数に入れずにそのまま入れた場合はエラーとなります。
この方法に関しては excel の想定している文字コードに変換していますが、一方で mac などの他の環境で Shift-JIS 対応が出来ていない環境に CSV が渡されることが想定される場合、かえって面倒なファイル生成となります。
その場合は次に紹介する方法の方がより良い対応かと思います。
文字コードの対応(その2)
2つ目の対応方法は、単なる UTF-8 ではなく、 BOM付きUTF-8 にするパターンです。
BOM付きUTF-8 にすることで UTF-8 のままでありながら、excel にも UTF-8 としても文字コード解釈を認識させることができます。
BOM付きUTF-8 にする方法としては、fopen 直後のファイルのまだ何も記載されていない先頭に対して、
fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF));
を実行する方法があります。
この方法を実行した上であとは従来の内容を文字コードを変換せずに記載することで、BOM付きUTF-8 である csv となります。