Laravel Snappyを使用して、請求書PDFを作成する方法

Laravel

はじめに

ビジネスにとって請求書は欠かせないものですが、手作業での作成や印刷、発送は時間と手間がかかります。

そこで、LaravelのSnappyを使ったPDF請求書の自動生成ツールを開発します。

このツールを使えば、手動作業に比べて時間と手間を大幅に削減することができます

PDFファイル形式で自動的に請求書を生成し、ダウンロードすることができます。

しかも、カスタマイズも容易で、ビジネスに合わせた請求書のデザインやレイアウトの変更も可能です

この記事では、LaravelとSnappyを使ってPDF請求書を簡単に作成する方法を解説します。

プログラミング初心者でも理解しやすいステップバイステップガイドで、手軽にPDF請求書の自動生成を始められます。

ビジネスの時間と手間を大幅に削減するために、このツールをぜひ活用してみてください。

本記事の対象者

  • Laravel初学者でLaravelでPDFダウンロードする方法を知りたい方
  • PDFのデザインをカスタマイズしたい方
  • PDFダウンロードを自動化したい方

また、本記事は基本的なHTML/CSSを学習していることを前提に進めます。

目標成果物

以下のような請求書をダウンロードできるシステムを開発します。

ライブラリ

LaravelでのPDFダウンロードはライブラリを使うのが一般的です。

Laravelではどのようなライブラリが用意されているのか、また、本記事で扱うライブラリはどのようなものなのかをまずはしっかりと理解しておきましょう。

Laravelでは以下のようなライブラリが用意されています。

  • Dompdf・・・導入が簡単だが、利用できるCSSに難がある
  • Snappy・・・導入が大変だが、利用できるCSSが多い
  • TCPDF・・・導入が簡単。座標を指示してPDFを作成する。現在はメンテナンスがされていない
  • mpdf・・・導入が簡単で、利用できるCSSも多く、最近人気が高い

これらライブラリの中でプロジェクトごとに最適な選択をするのがベストでしょう

Dompdfがおそらく一番情報多いですが、利用できるCSSが少ないため、あまりCSSを使わない場合は利用すると良いでしょう。

Snappyは導入時に少し癖がありますが、利用できるCSSは多いです。

TCPDFは一昔前まではPDFダウンロードのデファクトスタンダードだったようです。

mpdfは特に気になるところもなく、最近人気が高いようですね。

今回はデザインをある程度自由にカスタマイズできるようにしたく、筆者自身がよく使っているSnappyを使って実装していきます

公式サイトについては以下を確認してください。

GitHub - barryvdh/laravel-snappy: Laravel Snappy PDF
Laravel Snappy PDF. Contribute to barryvdh/laravel-snappy development by creating an account on GitHub.

プロジェクト作成

まずはLaravelプロジェクトを作成します。

$ composer create-project laravel/laravel=9.x laravel_pdf --prefer-dist

Snappy導入

次に、Snappyライブラリを導入します。

$ composer require barryvdh/laravel-snappy

Snappyの導入は少し癖があり、wkhtmltopdfのインストールが必要となります

windows 64bitをお使いの方はcomposerを利用してwkhtmltopdfをインストールできます。

$ composer require h4cc/wkhtmltopdf-amd64 0.12.x
$ composer require h4cc/wkhtmltoimage-amd64 0.12.x

macをお使いの方はwkhtmltopdfの公式サイトからダウンロードする必要があります。

wkhtmltopdf

次に、ServiceProviderに登録します。

config/app.phpのproviders配列に追加します。

config/app.php

'providers' => [
        ...
        Barryvdh\\Snappy\\ServiceProvider::class,
],

最後にファイルを公開します。

php artisan vendor:publish --provider="Barryvdh\\Snappy\\ServiceProvider"

これを行うと、config/snappy.phpが追加されます。

config/snappy.php

'pdf' => [
    'enabled' => true,
    'binary'  => env('WKHTML_PDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
    'timeout' => false,
    'options' => [],
    'env'     => [],
],

'image' => [
    'enabled' => true,
    'binary'  => env('WKHTML_IMG_BINARY', '/usr/local/bin/wkhtmltoimage'),
    'timeout' => false,
    'options' => [],
    'env'     => [],
],

composerでwkhtmltopdfをダウンロードした方は以下のように変更しましょう。

'pdf' => [
    'enabled' => true,
    'binary'  => base_path('vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64'),
    'timeout' => false,
    'options' => [],
    'env'     => [],
],

'image' => [
    'enabled' => true,
    'binary'  => base_path('vendor/h4cc/wkhtmltoimage-amd64/bin/wkhtmltoimage-amd64'),
    'timeout' => false,
    'options' => [],
    'env'     => [],
]

これでSnappyの導入が完了しました!

CSSの設定

必要な項目を入力できる入力画面と請求書PDFのインターフェースを作成していきますが、その前にCSSの設定だけしておきます。

Laravelのversion 9からはViteが使えるようになりました。

以前まではLaravel MixというものでCSS等を管理していましたが、最新のLaravelではViteに置き換わりました。

開発用サーバーにアクセスするために、vite.config.jsを以下のように変更しましょう。

vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    server: {
        hmr: {
            host: 'localhost'
        }
    },
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});

これでlocalhostでViteが使えるようになりました。

次に、package.jsonを確認してみましょう。

package.json

{
    "private": true,
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "axios": "^1.1.2",
        "laravel-vite-plugin": "^0.7.2",
        "lodash": "^4.17.19",
        "postcss": "^8.1.14",
        "vite": "^4.0.0"
    }
}

scriptsにあるように、npm run devを実行するとViteが起動するようになっています。

Viteを起動してみましょう。

$ npm run dev

Viteを起動しておくことで、ホットリロードが効き、自動的にresources/css/app.cssとresources/js/app.jsが生成され、CSSやJSが画面に適用されるようになります。

最後にこれらCSSとJSを画面に反映させるためにHTMLに組み込みます。

<head>
	@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

これでViteの設定完了です!

入力画面作成

まずは入力画面を作成していきます。

入力画面は以下の項目を入力できるようにします。

  • 案件名
  • 件名
  • 請求先
  • 請求日
  • 支払い期限
  • 単価設定(円)
  • 所要時間(h)

入力内容は必要に応じて適時変えるようにしましょう。

views配下にhome.blade.phpを作成します。

resources/views/home.blade.php

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Billing PDF</title>

        <!-- Fonts -->
        <link href="<https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap>" rel="stylesheet">
        <link rel="stylesheet" href="css/style.css">
        @vite(['resources/css/app.css', 'resources/js/app.js'])

        <style>
            body {
                font-family: 'Nunito', sans-serif;
            }
        </style>
    </head>
<body>
    <div class="container home">
        <div class="form">
            <h1>請求書作成</h1>
            <form action="{{ route('pdf_download') }}" method="GET">
                @csrf
                <div class="form-block">
                    <label for="project-name">案件名</label>
                    <input type="text" id="project-name" name="project-name">
                </div>
                <div class="form-block">
                    <label for="subject">件名</label>
                    <input type="text" id="subject" name="subject">
                </div>
                <div class="form-block">
                    <label for="billing-address">請求先</label>
                    <input type="text" id="billing-address" name="billing-address">
                </div>
                <div class="form-block">
                    <label for="billing-date">請求日</label>
                    <input type="date" id="billing-date" name="billing-date">
                </div>
                <div class="form-block">
                    <label for="due-date">支払い期限</label>
                    <input type="date" id="due-date" name="due-date">
                </div>
                <div class="form-block">
                    <label for="unit-price">単価設定(円)</label>
                    <input type="text" id="unit-price" name="unit-price">
                </div>
                <div class="form-block">
                    <label for="required-time">所要時間(h)</label>
                    <input type="text" id="required-time" name="required-time">
                </div>
                <button type="submit">請求書作成</button>
            </form>
        </div>
    </div>
</body>
</html>

HTMLの詳細については今回は割愛させていただきます。

次に、CSSを適用していきます。

resources/css/app.css

/* 共通 */
.right {
    text-align: right;
}
.left {
    text-align: left;
}
.center {
    text-align: center;
}
.bold {
    font-weight: bold;
}
.table-title {
    background-color: #282928;
    color: #fff;
}
.container {
    margin: 100px auto;
    width: 80%;
}

/* home */
.container .form {
    border: 1px solid black;
    box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.4);
    padding: 50px 100px;
}
.container .form .form-block {
    margin: 30px 0;
}
.container .form .form-block input {
    border: 1px solid black;
    margin-left: 10px;
    height: 30px;
    width: 400px;
}
.container .form button {
    border: 1px solid black;
    margin-top: 50px;
    padding: 10px 20px;
    cursor: pointer;
}

CSSについても詳細は割愛します。

また、今回はユーザーインターフェースにはこだわらなかったので簡素なものになっています。ご了承ください。

以下のような画面になります。

デザインはお好みで適時カスタマイズしましょう。

コントローラー作成

PDFを作成する処理を作りましょう。

コントローラーを作成します。

php artisan make:controller PdfController

app/Http/Controllers/PdfController.phpが作成されました。

それでは、PdfController.phpを編集していきます。

app/Http/Controllers/PdfController.php

<?php

namespace App\\Http\\Controllers;

use Illuminate\\Http\\Request;
use PDF;

class PdfController extends Controller
{
    public function viewPdf(Request $request)
    {
        $data = [
            'project_name' => $request->input('project-name'),
            'subject' => $request->input('subject'),
            'billing_address' => $request->input('billing-address'),
            'billing_date' => $request->input('billing-date'),
            'due_date' => $request->input('due-date'),
            'unit_price' => $request->input('unit-price'),
            'required_time' => $request->input('required-time'),
        ];

        return PDF::loadView('pdf.document', $data)
            ->inline();
    }
}

PDFファサードを使うので、以下をインポートしておきましょう。

use PDF;

実際にPDFを作成しているのは以下の処理です。

return PDF::loadView('pdf.document', $data)
    ->inline();

loadViewの第一引数はbladeテンプレート、第二引数はテンプレートに渡すデータを入れます。

inline()メソッドはプレビューを有効にする場合に使います。

ダウンロードしたい場合はdownload()としてください。

ルーティング作成

ルーティングはホーム画面と先ほど作成したPDF作成のルートがあれば良いです。

2本作っておきましょう。

<?php

use Illuminate\\Support\\Facades\\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('home');
});
Route::get('/pdf-download', [App\\Http\\Controllers\\PdfController::class, 'viewPdf'])->name('pdf_download');

請求書PDFデザイン作成

請求書PDFはresources/views/pdfのディレクトリに配置します。

resources/views/pdf/document.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>請求書PDF</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <div class="container pdf">
        <h1>請求書</h1>
        <table class="table1">
            <tr>
                <td>{{ $billing_address }} 御中</td>
                <td>請求日  {{ $billing_date }}</td>
            </tr>
            <tr>
                <td>下記の通り、ご請求申し上げます。</td>
            </tr>
        </table>
        <div class="row1">
            <table class="table2">
                <tr>
                    <th class="table-title left">件名</td>
                    <td>{{ $subject }}</td>
                </tr>
                <tr>
                    <th class="table-title left">支払期限</td>
                    <td>{{ $due_date }}</td>
                </tr>
                <tr>
                    <th class="table-title left">振込先</td>
                    <td>〇〇銀行 〇〇出張所 普通 1111111</td>
                </tr>
            </table>
            <div class="blank"></div>
            <div class="address">
                <p>株式会社テスト</p>
                <p>〒000-0000</p>
                <p>〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇</p>
                <p>TEL: 080-0000-0000</p>
            </div>
            <table class="table3">
                <tr>
                    <th style="font-size: 1.3em;">合計</th>
                    <td style="font-size: 1.3em;">{{ number_format((int)$unit_price * (int)$required_time * 1.1) }}円(税込)</td>
                </tr>
            </table>
            <table class="table4">
                <thead>
                    <tr>
                        <th class="table-title">概要</th>
                        <th class="table-title">単価</th>
                        <th class="table-title">所要時間</th>
                        <th class="table-title">金額</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="left">{{ $project_name }}</td>
                        <td class="right">{{ number_format((int)$unit_price) }}円</td>
                        <td class="right">{{ $required_time }}h</td>
                        <td class="right">{{ number_format($unit_price * $required_time) }}円</td>
                    </tr>
                    <tr>
                        <td class="left"></td>
                        <td class="right"></td>
                        <td class="center table-title bold">小計</td>
                        <td class="right">{{ number_format($unit_price * $required_time) }}円</td>
                    </tr>
                    <tr>
                        <td class="left"></td>
                        <td class="right"></td>
                        <td class="center table-title bold">消費税</td>
                        <td class="right">{{ number_format($unit_price * $required_time * 0.1) }}円</td>
                    </tr>
                    <tr>
                        <td class="left"></td>
                        <td class="right"></td>
                        <td class="center table-title bold">合計</td>
                        <td class="right">{{ number_format((int)$unit_price * (int)$required_time * 1.1) }}円</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

HTMLのポイントとしては、できるだけテーブルを使うようにするとすっきりとしたデザインを構築できます

次に、CSSを適用します。

/* PDF */
.pdf h1 {
    width: 100px;
    margin: 0 auto;
}
.pdf .table1 {
    margin-top: 50px;
    width: 100%;
}
.pdf .table1 th,td {
    padding: 10px;
}
.pdf .row1 {
    position: relative;
    margin-top: 30px;
}
.pdf .table2 {
    position: absolute;
    top: 0;
    left: 0;
    width: 60%;
    border-collapse: collapse;
}
.pdf .table2 th,td {
    padding: 10px;
}
.pdf .blank {
    width: 10%;
}
.pdf .address {
    position: absolute;
    top: 0;
    right: 0;
    width: 30%;
    padding: 0;
}
.pdf .address p:first-child {
    margin: 0;
}
.pdf .table3 {
    position: absolute;
    top: 200px;
    border-collapse: collapse;
    border-bottom: 1px solid black;
}
.pdf .table3 th,td {
    padding: 10px;
}
.pdf .table4 {
    position: absolute;
    top: 300px;
    border-collapse: collapse;
    width: 100%;
}
.pdf .table4 th,td {
    padding: 10px;
}

CSSは絶体位置で各要素を配置するように意識するとレイアウトを整えやすいです。

動作確認

さて、最後にホーム画面から適当な内容を入力して請求書作成ボタンを押してみましょう。

以下のような請求書PDFが作成されれば成功です。

請求書PDFのデザインはHTMLやCSSからカスタマイズ可能なので、必要に応じてカスタマイズしましょう。

最後に

LaravelのSnappyを使ったPDF請求書の自動生成ツールを開発することで、ビジネスに必要な請求書の作成にかかる時間と手間を大幅に削減することができました。

手動での作業に比べ、自動化によって生産性を高め、ビジネスの成果を上げることができます。

このツールは、カスタマイズ性も高く、ビジネスのニーズに合わせたレイアウトやデザインを自由自在に変更することができます。

今回の記事では、初心者でもわかりやすいステップバイステップガイドを紹介しました。

ぜひ、このツールを活用し、ビジネスの生産性を向上させてください。

コメント

タイトルとURLをコピーしました