EC関連のシステムなどでは請求書や領収書といった帳票類の出力が必要となる場合があります。
Webシステムにおける帳票類の出力では自動生成したPDFをダウンロードさせる方法が一般的かと思いますが、PHPのPDF出力用ライブラリとしては以下のようなものがあるようです。
- TCPDF
- snappy
- dompdf
- mpdf
TCPDFは以前何回か使用しました。
PDFのテンプレート(つまり固定フォーマット)に必要な情報を差し込む形で新しいPDFを生成できる点が特徴と言えます。
ただ、情報の差し込みに関する処理を個々に行う必要があり、特に差し込み位置や範囲を座標等で指定するため、記述量が相応に多く内容も複雑になる上にテンプレートの構造変更に対して柔軟性がないという弱点があります。
snappy、dompdf、mpdfはいずれもHTMLをベースにPDFを生成する方法になりますが、snappyはインストールが面倒(snappyが依存するwkhtmltopdfのインストールが面倒)らしく、dompdfは複雑なレイアウトに対応できない(使用できるCSSに制限がある)っぽいので、消去法でmpdfを試してみることにします。
なお、Laravelに関してはより便利に使用できるようにしたラッパーパッケージ「carlos-meneses/laravel-mpdf」が用意されているようなので、こちらを使用します。
インストール
インストールに関してはcomposerで簡単に実施できます。
# composer require carlos-meneses/laravel-mpdf
config/app.phpの変更
インストールしたmpdfパッケージをLaravel内で使用できるようにサービスプロバイダおよびファサードの設定を行います。
'providers' => [
...
/*
* Package Service Providers...
*/
Mccarlosen\LaravelMpdf\LaravelMpdfServiceProvider::class,
...
],
'aliases' => [
...
'PDF' => Mccarlosen\LaravelMpdf\Facades\LaravelMpdf::class,
...
],
config/pdf.phpの生成と設定
mpdfに関する設定は「config/pdf.php」で行います。
インストールしただけの状態では上記ファイルは存在しませんが、以下のコマンドを実行することで雛形が作成されます。
# php artisan vendor:publish --tag=mpdf-config
生成されたファイルの内容は以下になります。
<?php
return [
'mode' => '',
'format' => 'A4',
'default_font_size' => '12',
'default_font' => 'sans-serif',
'margin_left' => 10,
'margin_right' => 10,
'margin_top' => 10,
'margin_bottom' => 10,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'P',
'title' => 'Laravel mPDF',
'subject' => '',
'author' => '',
'watermark' => '',
'show_watermark' => false,
'show_watermark_image' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'watermark_image_path' => '',
'watermark_image_alpha' => 0.2,
'watermark_image_size' => 'D',
'watermark_image_position' => 'P',
'custom_font_dir' => '',
'custom_font_data' => [],
'auto_language_detection' => false,
'temp_dir' => storage_path('app'),
'pdfa' => false,
'pdfaauto' => false,
'use_active_forms' => false,
];
上記ファイルを自動的に生成してくれるのは良いですが、個々の設定内容に関する情報がネット含めてあまり見当たりません。
よって、入手できた僅かな情報と試行錯誤の結果から解釈した内容を以下に記録しておきます。
mode
文字コードや言語を指定するっぽいです。
日本語を使用したい場合は「ja」と指定する必要があるとの情報もあったのですが、試した限りでは後述する「custom_font_dir」「custom_font_data」の設定で適切な日本語フォントを指定すると本属性に「ja」を設定しなくても当該フォントが適用されるようでした(別途、HTML側でfont-familyの設定は必要)。
ただ、「ja」と指定しても支障はないようなので、とりあえずは「ja」と設定しておきます。
format
PDFの用紙サイズを指定するようです。
初期値は「A4」ですが、必要であれば変更します。
default_font_size
フォントサイズのデフォルト値を指定するようです。
単位は「pt」です。
default_font, custom_font_dir, custom_font_data
先に示した「mode」に「ja」を指定しておくと日本語表示もされますが、フォントが(種別は分かりませんが)今ひとつです。そこでIPAexフォントを新たにインストールし、これを使用するようにしてみます。
「resources/fonts」ディレクトリ配下に「IPAexゴシック」(ipaexg.ttf)をインストールし、「custom_font_dir」「custom_font_data」は以下のように設定します。
'custom_font_dir' => base_path('resources/fonts/'),
'custom_font_data' => [
'ipaexg' => [
'R' => 'ipaexg.ttf',
],
],
「custom_font_dir」ではフォントをインストールしたディレクトリを指定します。
「custom_font_data」ではフォントファイルを指定します。
フォントは複数指定できるため、「custom_font_data」は連想配列になっており、キーはフォントファミリー名で、値は連想配列です(フォントファミリーに対してはフォントを複数指定できるため)。
フォントファミリーに対応する連想配列では、キーはフォントの字体を示すキーワードで、上記例にある「R」(レギュラー)以外に「B」(ボールド)「I」(イタリック)「BI」(ボールド&イタリック)などがあるようです。
値にはフォントファイル名を指定します。
上記のように「custom_font_dir」「custom_font_data」を設定した上で「default_font」に「ipaexg」と指定すれば日本語表示には同フォントが使用される…と思ったのですが、実際には前述したデフォルトの日本語フォントの方が使用されてしまいます。
試行錯誤した結果、CSS側で「font-family」を指定するという方法でのみIPAexフォントが適用できています(つまり「default_font」は未使用)。
margin_left, margin_right, margin_top, margin_bottom
用紙全体に対する左右上下の余白を指定するようです。
単位は「mm」です。
orientation
用紙の向きを「P」(縦)または「L」(横)で指定するようです。
title
PDFファイルのプロパティ「タイトル」の内容を指定するようです。
適切な値を設定し直します。
subject, author
「title」と同様にPDFファイルのプロパティ「主題」および「作成者」の内容を指定するようです。
デフォルトでも空文字が設定されていますが、特に指定したい内容もないのでこのままで良いかと。
display_mode
PDFの表示モードを指定するようですが、初期値である「fullpage」以外の値(fullwidth, real, noneなど)を指定しても表示のされ方に特に違いは見つけられませんでした。
とりあえず「fullpage」(ページ全体を表示)で問題なさそうなので、このままにします。
auto_language_detection
「この設定を有効にすると、PDFの文字コードやフォント情報などを基に、最も可能性の高い言語が自動的に設定される」(by Gemini)とのことですが、実際に有効(true)にすると先に「custom_font_dir」「custom_font_data」で指定したフォントではなく、デフォルトのフォント(「mode」に「ja」を指定しただけの状態で使用されるフォント)で表示されました。
当然ながらそれでは都合が悪いので無効(false)のままにしておきます。
temp_dir
一時ファイルを生成するディレクトリを指定するようですが、具体的にどこにどのようなファイルが生成されるかは不明です(PDF生成自体はほとんど一瞬で完了してしまうので確認が困難)。
初期値の状態で動作的には問題なかったので、このままにします。
margin_header, margin_footer
PDFのヘッダーおよびフッターに関する余白を指定するようですが、そもそもデフォルトの状態ではヘッダー・フッターが表示されていません。
とりあえず放置で。
watermark関連
PDFにwatermark(透かし)を入れられるようですが、過去も含めてあまり必要な局面に遭遇していません。
初期値が「show_watermark」「show_watermark_image」ともにfalse(表示しない)なので、このままにします。
pdfa, pdfaauto
PDF/A(PDFの長期保存を目的とした規格)に準拠するかどうかを指定するらしいです(難しい…)。
ニーズがはっきりしないので、false(準拠しない)のままにしておきます。
use_active_forms
PDFフォームを使用するかどうかを指定できるようです。
そもそも「PDFフォーム」なる機能自体を初めて知ったのですが、PDFにフォームを埋め込んで、入力結果を所定のサーバに送信したり、PDFファイル自体に保存したりすることができるようですね(Gemini談)。
便利そうですが、直近の目的(帳票類の出力)ではあまり使用しそうにないので、false(使用しない)のままにしておきます。
Bladeテンプレートの作成
最初の方で書きましたが、mpdfはHTMLベースでPDFを生成する機能です。
HTMLの生成に関してはLaravel付属のテンプレートエンジンであるBladeを使用するようで、あらかじめ雛形となるBladeテンプレートを作成しておきます。
Bladeテンプレートの書式に関しては通常(Web表示)時と同じですが、スタイル定義をCSSファイルとして外付けにすると正しく読み込めないとの情報があったので、テンプレート内に直接スタイルを書く形にします。
なお、mpdfは再現性(元となるHTMLの見た目に忠実にPDFを生成すること)に優れているようですが、それでも期待したような結果にならないケースは見受けられました(根本的にスタイルの書き方に問題があった可能性もありますが)。
PDF生成自体は短時間で実行できますので、スタイルはトライアル&エラーで調整します。
PDF生成
PDFの生成に必要な処理内容は、大雑把に書けば以下程度です。
// PDFの生成
$data = [
'<キー>' => '<値>',
...
];
$pdf = PDF::loadView('pdf.receipt', $data);
// PDFの保存
$receiptFile = '<任意のファイル名>.pdf';
$pdf->save(Storage::disk('private')->path('receipts/'.$receiptFile));
生成に際して実行しているのは「PDF::loadView」メソッドのみです(ここでの「PDF」は「config/app.php」の「aliases」で設定したファサードです)。
第一引数はHTML生成に使用するBladeテンプレートで、上記にある「pdf.receipt」なる記述は「resources/views/pdf/receipt.blade.php」を意味します。
第二引数はテンプレートに反映するデータです。
Web表示における応答用HTML生成時に使用するviewヘルパと同等の用法なので、分かりやすいですね。
「loadView」メソッド実行により返されたオブジェクトでは生成したPDFに関する処理メソッドがいくつか用意されていますが、上記のように「save」メソッドで第一引数に指定したパスにファイルとして保存することができます。
最終的には同ファイルをダウンロードさせるなり、どこかに転送するなりすることになるかと思います。
まとめ
上記の通り、PDFを生成するためのロジック部分は極めて単純であるため、今回の作業工数の大部分はBladeテンプレートの作成(つまりはコーディング)で費やしました。
言い換えれば、コーディングに長けた人がテンプレート作成を担当すれば、相応に込み入ったレイアウトのPDFでもあまり苦労なく生成できるのではないかと思います。
同様にHTMLからPDFを生成するsnappyやdompdfは試していませんが、mpdfの用法のシンプルさやPDFの生成結果には今のところ満足しているので、特に支障がない限りPDF生成はmpdf一択で良いかと思っています。
もしmpdfで実現が難しいケースが出てきたら改めて他の機能も試してみたいと思いますが、その頃には別の優秀な機能が出てきているかもしれませんので、将来のことは将来判断するということで。