🧾 やりたいこと
Laravelアプリ上でチェックした複数のPDFを
・1つに結合
・自動で表示
・自動で印刷ダイアログ表示
する仕組みを作ります。
🧰 使用技術
Laravel 9.x
Browsershot(spatie/browsershot)
Node.js + Puppeteer
setasign/fpdi(PDF結合)
JavaScript(iframe onload → print)
✅ 1:Node.js & Puppeteer インストール手順(Apple Silicon対応)
✅ 2:Laravel側に Browsershot 導入
✅ 3:MacでのChromium設定(Google Chromeの代わり)
Puppeteer が使う Chromium を Mac 用に別途インストールします
Homebrew 版は壊れることがあるため、公式配布サイトから mac-arm 版を手動で入れます。(M1/2/3使用者)
🔹 3-1:Puppeteer が使う Chromium を Mac 用に別途インストールの手順
🔸 3-1-1:ダウンロード
下にスクロールし「Chromium for 64-bit macOS on Arm」の
👉 Archive 140.0.7310.0 (1489412) など最新を選んで .zip をダウンロード
🔸 3-1-2:解凍(chrome-mac フォルダになる)
chrome-mac.zip を解凍する
🔸 3-1-3:中の Chromium.app を /Applications に手動でドラッグ&ドロップ
🔹 3-2:初回起動で Gatekeeper を解除
Finder で /Applications を開き、Chromium.app を:
🔸 3-2-1:右クリック →「開く」→「開く」ボタンをクリック
🔸 3-2-2:「このMacには対応していません」や「壊れています」系が出なければOK
※一度開けば Puppeteer 経由でも起動できるようになります
インストール後にパス確認:
ls /Applications/Chromium.app/Contents/MacOS/Chromium
# → 結果:/Applications/Chromium.app/Contents/MacOS/Chromium
✅ 4:Chromium 設定を Laravel で行っていく
.env に以下を追加
# Browsershot(PDF、印刷)
CHROME_PATH="/Applications/Chromium.app/Contents/MacOS/Chromium"
↓
設定ファイル config/browsershot.php を作成
<?php
return [
'chrome_path' => env('CHROME_PATH'),
];
↓
コントローラーで反映方法例
Browsershot::html($html)
->setNodeBinary('/usr/local/bin/node')
->setIncludePath('/usr/local/bin')
->setChromePath(config('browsershot.chrome_path'))
->format('A4')
->showBackground()
->save($pdfPath);
↓ 実際のコントローラーは以下
🔹 4-1:Controller側の実装:選択された複数の領収書をPDF化して1つに結合し、印刷用の中継画面にリダイレクト
use App\Http\Requests\ReceiptRequest;
use App\Models\PaymentMethod;
use App\Services\ReceiptService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Spatie\Browsershot\Browsershot;
use ZipArchive;
use setasign\Fpdi\Fpdi;
// ⭐️ 選択された複数の領収書をPDF化して1つに結合し、印刷用の中継画面にリダイレクトする
public function generateAndPrintMultiple(Request $request)
{
// ✅ 情報を取得
$ids = $request->input('receipt_ids', []);
if(empty($ids)) {
return back()->with('error', '印刷する領収書を選択してください。');
}
/** @var \App\Models\User $user */
$user = Auth::user();
// ✅ 選択された各領収書を`HTML`から`PDF`に変換して一時保存し、ファイル名を配列にまとめている
$filenames = [];
foreach($ids as $id) {
$receipt = $user->receipts()->with(['paymentMethod', 'bentoDetails'])->findOrFail($id);
$html = view('pdf.receipt', compact('receipt'))->render();
$customerName = preg_replace('/[^\w\-]/u', '_', $receipt->customer_name);
$filename = "receipt_{$customerName}_{$id}.pdf";
$pdfPath = storage_path("app/public/tmp/{$filename}");
Browsershot::html($html)
->setNodeBinary('/usr/local/bin/node')
->setIncludePath('/usr/local/bin')
->setChromePath(config('browsershot.chrome_path'))
->format('A4')
->showBackground()
->save($pdfPath);
$filenames[] = $filename;
}
// ✅ PDFファイルの絶対パス(結合用に必要)
$pdfPaths = array_map(function ($filename) {
return storage_path("app/public/tmp/{$filename}");
}, $filenames);
// ✅ 結合後のPDF保存先
$mergedFilename = 'merged_receipt.pdf';
$mergedPath = storage_path("app/public/tmp/{$mergedFilename}");
// ✅ 結合処理
$this->mergePdfs($pdfPaths, $mergedPath); // $this = `generateAndPrintMultiple()メソッド`が定義されているクラス
// ✅ 中継ビューへリダイレクト(iframe + 印刷)
return redirect()->route('receipts.print.show', ['filename' => $mergedFilename]);
}
// ⭐️ 複数のPDFファイルを1つに結合して指定パスに保存する
public function mergePdfs(array $pdfPaths, string $mergedPath)
{
// ✅ Fpdfを継承したFpdiインスタンスを作成
$pdf = new class extends Fpdi {
// 何も追加しなくてOK(匿名クラス)
};
foreach($pdfPaths as $file) {
// 🔹 読み込むPDFファイルを指定して、ページ数などの情報を取得
$pageCount = $pdf->setSourceFile($file);
// 🔹 `$pdfPaths`の中にある`PDF($file)`の各ページを読み込んで、新しいPDFに1ページずつ同じサイズで追加
for($pageNo = 1; $pageNo <= $pageCount; $pageNo++) { // 例)A領収書:1ページ、B:1,B:2、C:1
$tplIdx = $pdf->importPage($pageNo); // 「$pageNo ページ目をコピー機に乗せる準備をする」
$size = $pdf->getTemplateSize($tplIdx);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($tplIdx);
}
}
// ✅ 作ったPDFを保存
$pdf->Output('F', $mergedPath); //(ファイルとして保存, ファイルパス)
}
🔹 4-2:setNodeBinary と setIncludePath も .env 切り替えを行う
setNodeBinary と setIncludePath も .env で柔軟に切り替えられるようにしておくことで、本番・開発・Apple Silicon など環境ごとの切り替えが容易になります。
.env に以下を追加
# Browsershot(PDF、印刷)
CHROME_PATH="/Applications/Chromium.app/Contents/MacOS/Chromium"
NODE_PATH="/usr/local/bin/node"
INCLUDE_PATH="/usr/local/bin"
↓
設定ファイル config/browsershot.php を作成
<?php
return [
'chrome_path' => env('CHROME_PATH'),
'node_path' => env('NODE_PATH'),
'include_path' => env('INCLUDE_PATH'),
];
↓
コントローラーで反映
Browsershot::html($html)
->setNodeBinary('/usr/local/bin/node')
->setIncludePath('/usr/local/bin')
->setChromePath(config('browsershot.chrome_path'))
->format('A4')
->showBackground()
->save($pdfPath);
✅ 5:fpdf & fpdi 導入(複数PDFをまとめる用)
ライブラリ | 主な用途 |
---|---|
setasign/fpdf |
PDFの新規作成(ゼロから) |
setasign/fpdi |
既存PDFの読み込み・結合・ページ抽出など |
fpdi はfpdf に依存 |
→ 両方インストールが基本(fpdi だけでは動かない) |
composer require setasign/fpdi
composer require setasign/fpdf
✅ 6:印刷ボタン
<form id="receipt-form" method="POST" target="_blank">
@csrf
<div class="flex gap-2 mb-4">
<button
type="submit"
onclick="submitForm('{{ route('receipts.bulkDownload') }}')"
class="text-white bg-gray-500 px-4 py-2 rounded hover:bg-gray-600">
✅ 選択したPDFを一括DL
</button>
<button
type="submit"
onclick="submitForm('{{ route('receipts.generate_and_print_multiple') }}')"
class="text-white bg-green-500 px-4 py-2 rounded hover:bg-green-600">
🖨️ 選択したPDFを一括印刷
</button>
</div>
{{-- ダウンロードチェックボックスエラーメッセージ --}}
@if(session('error'))
<div id="flash-message-error" class="text-red-500 mb-2">{{ session('error') }}</div>
@endif
{{-- 全て選択ボタン --}}
<div class="text-right">
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" id="select-all" class="form-checkbox text-indigo-600 cursor-pointer">
<span class="ml-1">すべて選択 / 解除</span>
</label>
</div>
<table class="whitespace-nowrap table-auto w-full text-left whitespace-no-wrap">
<thead>
<tr>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tr-lg"></th>
</tr>
</thead>
<tbody>
@foreach($receipts as $receipt)
<tr>
{{-- チェックボックス --}}
<td class="border-t-2 border-gray-200 px-4 py-3">
<input type="checkbox" name="receipt_ids[]" value="{{ $receipt->id }}" class="cursor-pointer">
</td>
</tr>
@endforeach
</tbody>
</table>
</form>
✅ 7:ルーティング設定
// 複数PDFの生成 → 中継ビューへリダイレクト
Route::post('/receipts/pdf/print-multiple', [ReceiptController::class, 'generateAndPrintMultiple'])->name('receipts.generate_and_print_multiple');
// 印刷表示(中継ビュー)
Route::get('/receipts/print/show/{filename}', [ReceiptController::class, 'showPrintView'])->name('receipts.print.show')
✅ 8:tmpディレクトリ(storage/app/public/tmp)作成
✅ 9:中継ビュー(PDF表示&印刷)
✅ 10: 中継ビュー用 Controller を追加
✅ 11:一時PDFのクリーンアップ(任意)
⭐️ 続き(本番環境)
⭐️関連
印刷