今回ですがCSVエクスポートの方法をお伝えします。
よく実装するものの、書き方を丸暗記するのは大変ですよね
そこで今回は記事にまとめて、サクッと実装できるようにしたいと思います。
CSVをダウンロードするコードの完成系
まずはCSVダウンロードの完成系になります。
インターフェース
<?php
namespace App\Services\Account;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\StreamedResponse;
interface ArticleCsvDownloadService
{
/**
* 記事一覧をCSVでダウンロードします
*
* @return StreamedResponse
*/
public function handle(): StreamedResponse;
}
一つのpublic
メソッドだけにしており、シンプルにしてます。
これでまずこのメソッドを読めばいんだとわかります。
命名で機能の内容がわかるため、メソッド名もhandle()
とシンプルな命名にしています。
handle()
という命名はよくみるので、このメソッドをまず見ればいいのだなとわかるようにしています。
実装クラス
続いて実装クラスです。詳しい内容は後で後述します。
まずはサクッと見てみましょう。
<?php
namespace App\Services\Account\Concrete;
use App\Models\Article;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ArticleCsvDownloadServiceImpl implements ArticleCsvDownloadService
{
/**
* {@inheritDoc}
*/
public function download(): StreamedResponse
{
ini_set("max_execution_time", 0);
$response = $this->generateCsv();
$response = $this->setHeaders($response);
return $response;
}
/**
* CSVを生成
* fputcsvでCSVに1行ずつ追加
*
* @return StreamedResponse
*/
private function generateCsv(): StreamedResponse
{
return new StreamedResponse(function () {
$stream = fopen('php://output', 'w');
fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF));
$this->fputHeader($stream);
$this->fputArticles($stream);
fclose($stream);
});
}
/**
* CSVのカラム名がわかるようにheaderを追加する。
*
* @param $stream
* @return void
*/
private function fputHeader($stream)
{
$header = array_keys($this->toRow());
fputcsv(
$stream,
$header,
);
}
/**
* fputcsvで1行ずつ口座ステータスを書き込みます
*
* @param $stream
* @return void
*/
private function fputArticles($stream): void
{
Article::chunk(100)->map(function (Article $article) use ($stream){
fputcsv(
$stream,
array_values($this->toRow($article))
);
});
}
/**
* 口座ステータスのCSVを配列に変換する
*
* @param Article|null $article 記事
* @return array
*/
private function toRow(?Article $article = null): array
{
return [
'id' => $article?->id,
'タイトル' => $article?->title,
'本文' => $article?->body,
];
}
/**
* ファイル名などをheadersにset
*
* @param StreamedResponse $response
* @return StreamedResponse
*/
private function setHeaders(StreamedResponse $response): StreamedResponse
{
$fileName = 'account_list_' . now()->format('Y-m-d-H:i') . '.csv';
$response->headers->set('Content-Type', 'application/octet-stream');
$response->headers->set('Content-Disposition', "attachment; filename={$fileName}");
return $response;
}
}
これが一連の流れです。
解説
それでは処理の解説をしていきます。
目次をつくる
/**
* {@inheritDoc}
*/
public function handle(): StreamedResponse
{
ini_set("max_execution_time", 0);
$response = $this->generateCsv();
$response = $this->setHeaders($response);
return $response;
}
handle()で意識しているのは、目次をつくることです。
- csvを生成
- csvダウンロードに必要なheadersをセット
- csvをダウンロードするレスポンスをreturn
というシンプルな目次構成で、処理の流れが掴みやすいです。
処理の流れがわかりやすい
仕様が掴めてよきです
この目次構成のおかげで、他のエンジニアがコードを読んでも理解できます。
エンジニアはコードを読む時間が長いので、こういった可読性を上げる工夫は重要だと考えています。
ini_set("max_execution_time", 0); // タイムアウトの時間設定を無限
上記では処理時間を無制限にしています。
ダウンロードする量が多いと360秒でもタイムアウトすることがあるので、いったんは0にしています。
この辺りは実装する機能に合わせて設定すると良いでしょう。
CSV生成処理
handle()では$this->generateCsv()
が記載されていました。
/**
* {@inheritDoc}
*/
public function handle(): StreamedResponse
{
ini_set("max_execution_time", 0);
$response = $this->generateCsv();
$response = $this->setHeaders($response);
return $response;
}
この中身を確認していきましょう。
/**
* CSVを生成
* fputcsvでCSVに1行ずつ追加
*
* @return StreamedResponse
*/
private function generateCsv(): StreamedResponse
{
return new StreamedResponse(function () {
$stream = fopen('php://output', 'w'); // ファイルを開く
fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF)); // BOMで文字化け防止
$this->fputHeader($stream); // 1行目にカラム名がわかるテーブルのヘッダーを追加
$this->fputArticles($stream); // 1行ずつ記事情報を記載する。
fclose($stream);
});
}
まずはStreamedResponseのインスタンスを生成します。
この中でcsvを作成していきます。
symfonyのStreamedResponseを利用します。
クラスStreamedResponseはResponseを拡張します。
StreamedResponse は、ストリーミングされた HTTP レスポンスを表します。
StreamedResponseはその内容に対してコールバックを使用します。
コールバックでは、 echo のような標準的な PHP 関数を使用してレスポンスをクライアントに返す必要があります。必要に応じて flush()関数を使用することもできます。
CSVはfopen
からfclose
までになります。
$stream = fopen('php://output', 'w');
何かcsvファイルがあればfopenで開きますが、今回は元となるcsvがないためphp://output
を開いています。'w'
は書き込み権限になります。
php:// — さまざまな入出力ストリームへのアクセス
phpマニュアル
php://output は書き込み専用のストリームで、 print および echo と同じ方法での出力バッファへの書き込みを許可します。
BOMで文字化け防止する
fwiteでbomをつけて文字化け防止をします。
fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF)); // BOMで文字化け防止
誰もが一度は文字化けさせたはず
BOMをつけないとWindowsで文字化けするなど頻繁になるので追加しています。
この辺りは下記の記事が参考になりそうなので、添付しておきます。
あとは書き出したい情報をfput
で1行ずつ追加していきます。
ここでは処理の流れをわかりやすくするためにメソッド分けしています。
$this->fputHeader($stream); // 1行目にカラム名がわかるテーブルのヘッダーを追加
$this->fputArticles($stream); // 1行ずつ記事情報を記載する。
1行目にカラム名を追加する
まずはカラム名をわかるようにします。
挿入するデータはtoRow()
で定義をしています。
/**
* 口座ステータスのCSVを配列に変換する
*
* @param Article|null $article 記事
* @return array
*/
private function toRow(?Article $article = null): array
{
return [
'id' => $article?->id,
'タイトル' => $article?->title,
'本文' => $article?->body,
];
}
配列だとどの値を書いているのか
わかりやすいね
この配列のkeyとなるid
とタイトル
、本文
が今回必要なカラム名です。
このkeyを取得するために、array_keys()
を利用します。
array_keys($this->toRow());
引数の$articleいらないの?
$articleがないとエラーになってしまうので、null安全オペレータを利用しています。
'タイトル' => $article->title, // ->だとエラー
'タイトル' => $article?->title, // ?->だとnullを返すので、エラーが発生しない
これでkeyを取得できますね。
では実際に利用しているfputHeader()
をみていきましょう。
/**
* CSVのカラム名がわかるようにheaderを追加する。
*
* @param $stream
* @return void
*/
private function fputHeader($stream)
{
$header = array_keys($this->toRow()); // カラム名を取得
fputcsv(
$stream,
$header,
);
}
説明した通り、array_keys
でカラム名を取得し、それをfputcsv()
で入力しています。
これで1行目にカラム名を入力できましたね。
1行ずつ記事情報を入力する。
map()処理でfputcsv()を実行し、1行ずつ記事情報を記載していきます。
/**
* fputcsvで1行ずつ口座ステータスを書き込みます
*
* @param $stream
* @return void
*/
private function fputArticles($stream): void
{
Article::chunk(100)->map(function (Article $article) use ($stream){
fputcsv(
$stream,
array_values($this->toRow($article))
);
});
}
csvを閉じる
これで入力したい情報を記載終えので、fclose()で閉じます。
fclose($stream);
csv生成処理の振り返り
これでcsvの生成箇所は理解できたはずです。
/**
* CSVを生成
* fputcsvでCSVに1行ずつ追加
*
* @return StreamedResponse
*/
private function generateCsv(): StreamedResponse
{
return new StreamedResponse(function () {
$stream = fopen('php://output', 'w');
fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF));
$this->fputHeader($stream);
$this->fputArticles($stream);
fclose($stream);
});
}
ダウンロードするためにheadersをセット
さてcsvを作れたところで、ダウンロードするための準備に取り掛かります。
/**
* {@inheritDoc}
*/
public function download(): StreamedResponse
{
ini_set("max_execution_time", 0);
$response = $this->generateCsv();
$response = $this->setHeaders($response); // ダウンロードするためにheadersを操作
return $response;
}
headersをセットしている、$this->setHeaders($response)
をみていきましょう。
/**
* ファイル名などをheadersにset
*
* @param StreamedResponse $response
* @return StreamedResponse
*/
private function setHeaders(StreamedResponse $response): StreamedResponse
{
$fileName = 'account_list_' . now()->format('Y-m-d-H:i') . '.csv'; //ファイル名
$response->headers->set('Content-Type', 'application/octet-stream'); //MIMEを設定
$response->headers->set('Content-Disposition', "attachment; filename=${fileName}"); // ダウンロード方式とファイル名を設定
return $response;
}
まずはMIMEを設定します。
$response->headers->set('Content-Type', 'application/octet-stream'); //MIMEを設定
そして、実際にダウンロード設定をします。
ファイル名を決めて、setします。
$fileName = 'account_list_' . now()->format('Y-m-d-H:i') . '.csv'; //ファイル名を決める
$response->headers->set('Content-Disposition', "attachment; filename=${fileName}");
attachment
は即座にダウンロードします。inline
の場合プレビューが表示されますが、PDFでないとできません。
これでheader情報もセットできましたので以上となります。
$responseをreturn
/**
* {@inheritDoc}
*/
public function handle(): StreamedResponse
{
ini_set("max_execution_time", 0);
$response = $this->generateCsv();
$response = $this->setHeaders($response);
return $response;
}
responseを返してcsvをダウンロードしましょう
終わり
リファクタもしてあるので、読みやすく書いたつもりですが、いかがだったでしょうか?
この辺りは本当によく書くので、見本としておいておくと実装効率早くなるので、よくなると思います。
import処理も書いていきたいと思いますので、今後の記事も楽しみにしておいてくださいね
コメント