🧾 やりたいこと
Laravelアプリ上でチェックした複数のPDFを
・1つに結合
・自動で表示
・自動で印刷ダイアログ表示
する仕組みを作ります。
🧰 使用技術
Laravel 9.x
Browsershot(spatie/browsershot)
Node.js + Puppeteer
setasign/fpdi(PDF結合)
JavaScript(iframe onload → print)
📦 1. インストール
✅ Node.js & Puppeteer インストール手順
✅ Laravel側に Browsershot 導入
✅ fpdf & fpdi 導入
ライブラリ | 主な用途 |
---|---|
setasign/fpdf |
PDFの新規作成(ゼロから) |
setasign/fpdi |
既存PDFの読み込み・結合・ページ抽出など |
fpdi はfpdf に依存 |
→ 両方インストールが基本(fpdi だけでは動かない) |
composer require setasign/fpdi
composer require setasign/fpdf
🖨️ 2. 印刷ボタン
/resources/views/receipts/index.blade.php
<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>
🏗 3. ルーティング設定
web.php
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')
🧑💻 4. # tmpディレクトリ(storage/app/public/tmp)作成
🧠 5. Controller側の実装:選択された複数の領収書をPDF化して1つに結合し、印刷用の中継画面にリダイレクト
/Http/Controllers/ReceiptController.php
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')
->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); //(ファイルとして保存, ファイルパス)
}
🖼 6. 中継ビュー(PDF表示&印刷)
🧠 7. 中継ビュー用 Controller を追加
🗑 8. 一時PDFのクリーンアップ(任意)
本番でインストール必須
① PHPパッケージ(Composer)
パッケージ名 | 用途 |
---|---|
spatie/browsershot |
HTML → PDF化(Puppeteer使う) |
setasign/fpdi |
複数PDFを結合 |
illuminate/support 等 |
Laravelコア(通常すでに入ってる) |
# 本番環境で依存パッケージをインストール(最適化あり)
composer install --no-dev --optimize-autoloader
# 必要なら個別に(まだ入れてない場合)
composer require spatie/browsershot
composer require setasign/fpdi
composer require setasign/fpdf
② Node.js & Puppeteer
ツール | 用途 |
---|---|
Node.js | Puppeteerを動かすJS実行環境 |
Puppeteer(Headless Chrome) | ブラウザレンダリングしてPDF生成 |
# Node.js(v18など安定版)をインストール
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
# Puppeteerをグローバルにインストール(またはプロジェクトに)
npm install -g puppeteer
ストレージディレクトリの準備
PDFの一時保存先 /storage/app/public/tmp に書き込める必要あり。
# 一時PDFを保存するディレクトリを作成
mkdir -p storage/app/public/tmp
# 書き込み権限を付与(LaravelのWebサーバーユーザーに応じて調整)
chmod -R 775 storage/app/public/tmp
古いPDFを自動削除(スケジュール登録)
関連