#26 Laravel基礎
CSVインポート・エクスポート
CSVエクスポートの実装
一覧データをCSVファイルでダウンロードできる機能は、管理画面でよく求められます。
Responseで直接CSVを返す方法(シンプルな場合)
// 📁 app/Http/Controllers/PostController.php
use Symfony\Component\HttpFoundation\StreamedResponse;
public function exportCsv(): StreamedResponse
{
$posts = Post::with('user')->published()->latest()->get();
$headers = [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="posts_' . now()->format('Ymd') . '.csv"',
];
$callback = function () use ($posts) {
$handle = fopen('php://output', 'w');
// BOM付きUTF-8(Excelで文字化けしない)
fwrite($handle, "\xEF\xBB\xBF");
// ヘッダー行
fputcsv($handle, ['ID', 'タイトル', '著者', '作成日']);
// データ行
foreach ($posts as $post) {
fputcsv($handle, [
$post->id,
$post->title,
$post->user->name,
$post->created_at->format('Y-m-d'),
]);
}
fclose($handle);
};
return response()->stream($callback, 200, $headers);
}
ルートとビューに追加します。
// 📁 routes/web.php
Route::get('/posts/export-csv', [PostController::class, 'exportCsv'])
->name('posts.export-csv')
->middleware('auth');
{{-- ダウンロードリンク --}}
<a href="{{ route('posts.export-csv') }}">CSVダウンロード</a>
CSVインポートの実装
フォームの作成
{{-- 📁 resources/views/posts/import.blade.php --}}
<form method="POST" action="{{ route('posts.import') }}" enctype="multipart/form-data">
@csrf
<input type="file" name="csv_file" accept=".csv">
@error('csv_file')
<p>{{ $message }}</p>
@enderror
<button type="submit">インポート</button>
</form>
コントローラー
// 📁 app/Http/Controllers/PostController.php
public function import(Request $request)
{
$request->validate([
'csv_file' => 'required|file|mimes:csv,txt|max:2048',
]);
$file = $request->file('csv_file');
$handle = fopen($file->getPathname(), 'r');
// BOMを除去(Excelで作ったCSVに含まれることがある)
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
// ヘッダー行を読み飛ばす
fgetcsv($handle);
$imported = 0;
$errors = [];
while (($row = fgetcsv($handle)) !== false) {
// $row = ['タイトル', '本文', 'ステータス']
[$title, $body, $status] = $row;
if (empty($title) || empty($body)) {
$errors[] = "タイトルまたは本文が空の行をスキップしました";
continue;
}
Post::create([
'title' => trim($title),
'body' => trim($body),
'status' => in_array($status, ['draft', 'published']) ? $status : 'draft',
'user_id' => auth()->id(),
]);
$imported++;
}
fclose($handle);
$message = "{$imported}件インポートしました。";
if ($errors) {
$message .= ' ' . count($errors) . '件スキップ。';
}
return redirect()->route('posts.index')->with('success', $message);
}
大量データのインポートはキューを使う
CSVが大きい場合は直接処理せずキューに投げます。
// 📁 app/Jobs/ImportPostsCsv.php
class ImportPostsCsv implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public string $filePath, // ストレージに保存したファイルパス
public int $userId
) {}
public function handle(): void
{
$handle = fopen(storage_path("app/{$this->filePath}"), 'r');
// ...CSVを処理...
fclose($handle);
// 処理完了後にファイルを削除
Storage::delete($this->filePath);
}
}
コントローラーでファイルを一時保存してジョブをディスパッチします。
public function import(Request $request)
{
$path = $request->file('csv_file')->store('imports');
ImportPostsCsv::dispatch($path, auth()->id());
return redirect()->back()->with('success', 'インポートをバックグラウンドで開始しました。');
}
まとめ
- CSVエクスポートは
response()->stream()でメモリ効率よく実装できる fputcsv()でCSVを書き込む。BOM付きにするとExcelで文字化けしない- インポートは
fgetcsv()で1行ずつ読み込みPost::create()でDBに保存する - 大量データのインポートはキューで非同期処理するのが安全
次回はフィーチャーテストの書き方を学びます。