Laravel で API リソースを使って CSV ダウンロードする


はじめに

Laravel で CSV ダウンロードするときのアーキテクチャはいくつかある。

けど、コントローラからモデルをとって、整形して、CSV にするなら、

標準の JSON API リソースが JSON じゃなくて CSV になるだけでもよいのではないかと思った。

そして意外となかったので、書いてみます。


CSV API リソースを実装する

標準の JSON API リソースの実装を見て、最低限な次の機能を実装しました。


  • CSV だと複数のレコードを扱うように思うので、コレクションを扱う ResourceCollection のみ

  • 整形とダウンロードのレスポンス生成のみ(ほかにも機能があるっぽい)


ResourceCollection.php

<?php

namespace App\Http\Resources\Csv;

use Illuminate\Contracts\Support\Responsable;

abstract class ResourceCollection implements Responsable
{
protected $collection;

public function __construct($resource)
{
$this->collection = $resource;
}

public function toResponse($request)
{
return response()->streamDownload(function () {
$file = new \SplFileObject('php://output', 'wb');

$header = $this->attributes();
if (! empty($header)) {
$file->fputcsv($this->convertCharset($header));
}

foreach ($this->toArray() as $fields) {
$file->fputcsv($this->convertCharset($fields));
}
}, $this->fileName());
}

private function convertCharset($fields)
{
return collect($fields)
->map(function ($item) {
return mb_convert_encoding($item, 'SJIS-win', mb_internal_encoding());
})
->all();
}

/**
* 見出しがあれば返します
*/

protected function attributes()
{
return [];
}

/**
* 整形した配列を返します
*/

abstract protected function toArray();

/**
* ダウンロードファイルの名前があれば返します
*/

protected function fileName()
{
return 'download.csv';
}
}



使ってみる

注文情報を CSV に書き出すことを考えてみましょう。


コントローラ

ほとんど JSON のときと同じです。


OrderController.php

<?php

namespace App\Http\Controllers;

use App\Order;
use App\Http\Resources\ReportResource;

class OrderController extends Controller
{
public function report()
{
return new ReportResource(
Order::query()
->where('is_canceled', false)
->get()
);
}
}



API リソース

JSON のときと同じく、toArray をオーバライドして整形します。

加えて、attributes で見出しを、fileName でダウンロードファイル名を、オーバライドして指定することもできます。


ReportResource.php

<?php

namespace App\Http\Resources;

use App\Http\Resources\Csv\ResourceCollection;

class ReportResource extends ResourceCollection
{
protected function toArray()
{
$this->collection->load(['user', 'products']);

return $this->collection
->map(function ($order) {
return [
$order->id,
$order->user->id,
$order->user->name,
$order->products->sum('price'),
];
})
->all();
}

protected function attributes()
{
return [
'注文ID',
'会員ID',
'会員名',
'注文金額合計',
];
}

protected function fileName()
{
return '注文一覧.csv';
}
}



ダウンロード例


注文一覧.csv

注文ID,会員ID,会員名,注文金額合計

1,1,ほげほげ,1000
2,1,ほげほげ,1500
3,2,ふがふが,1000
4,2,ふがふが,2000


おわりに

JSON のときと同じ使い方ができるので、覚えることが少ないし、標準にも近いかもしれない。

すっきり!