PhpSpreadsheetで実務的なExcel帳票を出力するハンズオン
概要
業務システムでは「画面で見られればいい」で済まず、請求書・納品書・月次集計表といった Excel 帳票の出力を求められる場面が頻繁にあります。PHP で Excel ファイルを生成するデファクトが PhpSpreadsheet です。
この記事では、単にセルに値を入れるだけでなく、実務でそのまま提出できるレベルの帳票(罫線・結合セル・数値書式・集計行・テンプレート流し込み)を作るための実装パターンを、ハンズオン形式でまとめます。題材は「請求書」と「月次売上集計表」です。
環境
- PHP 8.2 / 8.3 以上(PhpSpreadsheet 5.x は PHP 8.1 でも動きますが、新しめの 5.x は 8.2 以上が必要で、実運用は 8.3+ を推奨)
- PhpSpreadsheet 5.x 系(最新は 5.10 系。2.x〜5.x でも本記事で使う帳票系の基本 API は共通です)
- Composer、GD / Zip / mbstring 拡張
composer require phpoffice/phpspreadsheet
実務でハマるポイント
PhpSpreadsheet は機能が豊富な分、素直に書くと以下の問題に当たります。
- セル指定を
A1,B1と文字列で書くと、列が増減したときに全滅する - 数値が文字列として入り、Excel 側で「緑の三角」が出て SUM が効かない
- 日本円の金額に桁区切りが入らず体裁が悪い
- 大量行でメモリを食い潰す
- 既存テンプレート(.xlsx)に値だけ流し込みたいのに書式が崩れる
これらを順に解決します。
基本:新規ブックを作ってセルに書き込む
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('請求書');
$sheet->setCellValue('A1', '御請求書');
$writer = new Xlsx($spreadsheet);
$writer->save('invoice.xlsx');
// ループで大量生成するなら明示的に解放
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
PhpSpreadsheet はオブジェクトの相互参照が強く GC が効きにくい場面があります。バッチで連続生成するなら disconnectWorksheets() を忘れずに。
列を定数で扱う:文字列ハードコーディングを避ける
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
$col = Coordinate::stringFromColumnIndex(3); // 'C'
$headers = ['日付', '品目', '数量', '単価', '金額'];
foreach ($headers as $i => $label) {
$colLetter = Coordinate::stringFromColumnIndex($i + 1);
$sheet->setCellValue("{$colLetter}1", $label);
}
列を意味のある名前で管理しておくと、仕様変更で列が増えても壊れにくくなります。
$columns = ['date'=>1, 'item'=>2, 'quantity'=>3, 'price'=>4, 'amount'=>5];
function cell(string $key, int $row, array $columns): string {
return Coordinate::stringFromColumnIndex($columns[$key]) . $row;
}
数値・日付を「数値として」書き込む
帳票でいちばん事故が多いのがここです。setCellValue('C2', '1000') のように文字列を渡すと左寄せ・文字列扱いになり SUM が効きません。型を明示します。
use PhpOffice\PhpSpreadsheet\Cell\DataType;
$sheet->setCellValueExplicit('D2', 1200, DataType::TYPE_NUMERIC);
日付は Excel のシリアル値に変換して入れ、表示書式を別で当てると崩れにくいです。
use PhpOffice\PhpSpreadsheet\Shared\Date;
$sheet->setCellValue('A2', Date::PHPToExcel(new DateTime('2026-10-24')));
$sheet->getStyle('A2')->getNumberFormat()->setFormatCode('yyyy/mm/dd');
数値書式:円・パーセント・桁区切り
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
$sheet->getStyle('E2:E100')->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); // 1,234
$sheet->getStyle('E2:E100')->getNumberFormat()
->setFormatCode('"¥"#,##0'); // ¥1,234
$sheet->getStyle('F2')->getNumberFormat()
->setFormatCode('0.0%'); // 10.5%
書式はセル単位でなく範囲にまとめて当てると、コードが軽くなり行追加にも強くなります。
罫線・結合・配置:帳票らしく見せる
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
// タイトルを A1:E1 で結合して中央寄せ
$sheet->mergeCells('A1:E1');
$sheet->setCellValue('A1', '御請求書');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(18);
$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
// 明細テーブルに格子罫線
$sheet->getStyle('A5:E20')->getBorders()->getAllBorders()
->setBorderStyle(Border::BORDER_THIN);
// ヘッダー行に背景色
$sheet->getStyle('A5:E5')->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()->setARGB('FFE0E0E0');
列幅は帳票では固定値が無難です(setAutoSize(true) は行数が多いと遅い)。
$sheet->getColumnDimension('A')->setWidth(12);
$sheet->getColumnDimension('B')->setWidth(30);
明細行を流し込み、合計を数式で入れる
合計は PHP 側で足し算した値を書くのではなく、Excel の数式として入れるのがおすすめです。利用者がセルを編集しても合計が追従し、「数字が合わない」クレームを防げます。
$items = [
['date'=>'2026-10-01', 'name'=>'設計作業', 'qty'=>10, 'price'=>8000],
['date'=>'2026-10-05', 'name'=>'実装作業', 'qty'=>20, 'price'=>8000],
['date'=>'2026-10-20', 'name'=>'テスト作業', 'qty'=>5, 'price'=>8000],
];
$startRow = 6;
$row = $startRow;
foreach ($items as $item) {
$sheet->setCellValue("A{$row}", $item['date']);
$sheet->setCellValue("B{$row}", $item['name']);
$sheet->setCellValueExplicit("C{$row}", $item['qty'], DataType::TYPE_NUMERIC);
$sheet->setCellValueExplicit("D{$row}", $item['price'], DataType::TYPE_NUMERIC);
$sheet->setCellValue("E{$row}", "=C{$row}*D{$row}"); // 数量 × 単価
$row++;
}
$lastRow = $row - 1;
$sheet->setCellValue("D{$row}", '小計');
$sheet->setCellValue("E{$row}", "=SUM(E{$startRow}:E{$lastRow})");
$subtotalRow = $row; $row++;
$sheet->setCellValue("D{$row}", '消費税(10%)');
$sheet->setCellValue("E{$row}", "=E{$subtotalRow}*0.1");
$taxRow = $row; $row++;
$sheet->setCellValue("D{$row}", '合計');
$sheet->setCellValue("E{$row}", "=E{$subtotalRow}+E{$taxRow}");
$sheet->getStyle("E{$startRow}:E{$row}")->getNumberFormat()->setFormatCode('"¥"#,##0');
テンプレート方式:既存の .xlsx に値だけ流し込む
実務で最も強力なのが、デザイン済みテンプレートを読み込んで値だけ差し替える方式です。ロゴ・押印欄・固定文言はデザイナーや事務方に作ってもらい、開発側はプレースホルダを置換します。
use PhpOffice\PhpSpreadsheet\IOFactory;
$spreadsheet = IOFactory::load(__DIR__ . '/template.xlsx');
$sheet = $spreadsheet->getActiveSheet();
$replacements = [
'{{customer}}' => '株式会社サンプル',
'{{issued_at}}' => '2026-10-24',
'{{total}}' => 330000,
];
foreach ($sheet->getRowIterator() as $rowObj) {
foreach ($rowObj->getCellIterator() as $cell) {
$value = $cell->getValue();
if (is_string($value) && isset($replacements[$value])) {
$cell->setValue($replacements[$value]);
}
}
}
書式崩れのリスクが激減し、見た目の調整を開発タスクから切り離せます。帳票要件が出たら、まずこの方式で組めないか検討するのがおすすめです。
大量データ対策とフレームワーク連携
- 数式を文字列で入れても PhpSpreadsheet 自身は計算結果を持ちません。サーバ側で計算値が必要なら
getCalculatedValue()を使います - 巨大範囲への一括スタイル適用は重いので、使う範囲に限定します
- 十万行以上なら xlsx をあきらめて CSV ストリーミングへ切り替える判断も設計のうちです
Laravel では一時ファイルに書かず、ストリームで返すと I/O が減ります。
use Symfony\Component\HttpFoundation\StreamedResponse;
$response = new StreamedResponse(function () use ($spreadsheet) {
(new Xlsx($spreadsheet))->save('php://output');
$spreadsheet->disconnectWorksheets();
});
$response->headers->set('Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment; filename="invoice.xlsx"');
return $response;
確認方法
- 生成した .xlsx を Excel / LibreOffice / Google スプレッドシートの3つで開き、罫線・結合・数値書式が崩れないか確認する
- 合計セルの数式参照範囲がズレていないか確認する
- 数値セルが右寄せ(数値扱い)になっているか、緑三角が出ていないか確認する
- 大量行で
memory_get_peak_usage(true)をログに出し、想定メモリに収まるか確認する
まとめ
実務的な Excel 帳票を PhpSpreadsheet で作る勘所は4点です。
- 列は文字列ハードコーディングせず、インデックス or 名前で管理する
- 数値・日付は型を明示し、書式は範囲にまとめて当てる
- 合計は PHP の計算結果ではなく Excel 数式で入れる
- 凝ったレイアウトはテンプレート .xlsx 方式に逃がす
特に「テンプレート流し込み」は、見た目の調整を開発から切り離せるので保守コストを大きく下げられます。帳票要件が出たら、まずテンプレート方式で組めないかを検討することをおすすめします。
Sources: PHPOffice/PhpSpreadsheet Releases
