Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編⑱】PDFファイル化

A子

前回、ボックス予想のための機能を作ったじゃん?

B美

そうね

A子

あれってさぁ
いちいち結果を紙にメモっておくのが、なにげに面倒なんだよね

C菜

印刷すれば良いんじゃないでしょうか~?

A子

わざわざ印刷するのは、紙がもったいない…

B美

貧乏性ね(苦笑)

てか、メモるほうの紙はもったいなくないの?

A子

いや、それは要らない紙(印刷に失敗した裏紙等)を四つ切にして使ってるから良いんだよ

そんなことより、Webアプリで出力した結果を何らかのファイルにして、保存しておくことってできないの?

C菜

ブラウザ画面上で右クリックして、メニューから「名前を付けて保存」を選べば良いのでは~?

A子

うーん、それも考えてみたんだけどさ
HTMLファイルでしか保存できないみたいなんだよね

さらにその場合、CDNでアクセスしているBulma(CSSフレームワーク)も無効化されちゃうし…

B美

そうねぇ

だったら、PDFファイルにしてみる?
案外、簡単よ

C菜

そんなのがブラウザの機能として、あるんですか~?

それとも何らかの拡張機能をオプションとしてインストールするとか~?

B美

どっちも不正解

正解は、(CakePHPのプログラムとして)PDFファイル化するコードを組むの

A子

そういえば、国税庁が提供している確定申告のWebページって、計算結果をPDFファイルとして保存できるようになってるね

ああいうやつ?

B美

その通り

何かを印刷したい場合って、いったんPDFファイル化するのは(Webの世界では)よくあることよ

C菜

良いですねぇ~

「案外、簡単」ということは、PHPのライブラリがあるってことでしょうか~?

B美

そういうこと
いくつかの選択肢があるわよ

私は昔「TCPDF」をよく使ってたんだけど、今は「mPDF」ばかり使ってるわ
(好みの問題なんだけど…)

A子

よくわかんないから、B美のお勧めで良いよ

その「mPDF」ってやつ?

B美

OK
それじゃ、「MATE端末」を開いてから、composerでライブラリをインストールしてみましょう

cd html/numapp[Enter]
composer require mpdf/mpdf[Enter]

あと、テンポラリ領域を設定して、フォントのダウンロードとその配置も必要だけどね


C菜

なんだか面倒そうです~

B美

面倒か、そうでないかで言えば面倒だけどね(苦笑)

まぁ、やってみましょう

A子

フォントのダウンロード」って何で必要なの?

てか、それってOSに内蔵されてるんじゃないの?

B美

PDFって、(どこでも問題なく表示できるように)ファイルの中にフォントを埋め込むのよ

その場合、OS付属のフォントではライセンスの問題が発生するからね
(要するに、著作権的な問題…)

C菜

今からダウンロードしようとしてるフォントって、完全にフリーなんですか~?

B美

そうよ

なにしろ「独立行政法人 情報処理推進機構(IPA)」が公開しているフォントだからね
(通称「IPAフォント」…正確には「IPAexフォント」と呼ばれます)

A子

ん?
IPAって、「ITパスポート」試験や「基本情報技術者」試験の主催じゃなかったっけ?

B美

正解!
それらの試験が『国家試験』であることからも、IPA自体が国の機関であることが分かるでしょう?

さて、それじゃブラウザ(Linux上のFirefox)を使ってダウンロードしてみましょう
(「IPAフォント」というキーワードでググってね)

ダウンロードしたものが以下の「IPAexfont00401.zip」というファイルよ

C菜

ZIPファイルなので、ここに展開してみますね~



B美

「IPAexfont00401」ディレクトリの中に二つのTTFファイルがあるでしょ?

それがフォントファイルよ
(「ipaexg.ttf」がゴシック体で、「ipaexm.ttf」が明朝体ね)

A子

えっと、その二つをCakePHPのプロジェクトファイルの中にコピーするの?

B美

そうよ

んで、フォントの置き場所って、(本来は)「numapp/vendor/mpdf/mpdf/ttfonts」なんだけど、「vendor」以下をいじくるのはお勧めしないわ

A子

んじゃ、どうすんのさ?

B美

「numapp」ディレクトリの下に「fonts」ディレクトリを作成して、そこに配置しましょう

あ、ファイルのパーミッションを(「700」から)「644」に変更しないといけないからね
それを忘れないように!

B美

続けて、テンポラリ領域を作りましょう

「numapp/tmp」の下に「mpdf」ディレクトリを作って、そのオーナーをWebサーバ(www-data)にする
あと、ディレクトリ自体のパーミッションを「755」に変更

C菜

root権限が必要なんですね~

A子

実行結果を見る限り、忘れてたみたいだけどね(笑)

B美

うっ、うるさいわい

ちょっと指が滑っただけよ

C菜

これで終わりですか~?

B美

あとはCakePHPのコードを打ち込んでいくだけよ

あ、コントローラについては、専用のものを作っておきましょうか

A子

んじゃ、(いつも通り)「src/Controller/PdfController.php」の中身を全部削除しておこう

B美

まずはuse文ね

use Mpdf\Mpdf;
use Mpdf\HTMLParserMode;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;

の四つを記述してね

あと、全体の流れを先に言っておくと…

1.フォーム送信された値で再計算
2.Mpdfクラスをインスタンス化
3.CSSを定義(変数に格納)
4.HTMLによるコンテンツを定義(変数に格納)
5.WriteHTMLメソッドの引数として(の変数)を渡したあと、PDFデータの書き出しを行う
6.レスポンスを返す

…って感じかな

A子

んん?
CSSとHTMLってことは、このPDFファイルってWebページみたいに作るってこと?

B美

そうよ

この特徴こそが、私が「mPDF」というライブラリを推す理由なのよね
(「TCPDF」のほうって、座標指定が必要だからめっちゃ大変なの)

C菜

良いですね~

作りやすそうです~

B美

あっ、その前に…
前回作った「TopController」の中の「combinationResult」メソッドだけど、中核部分を別メソッドに切り出しておきましょう

なぜなら「PdfController」の中でも全く同じ処理をするからね

C菜

ということは~

その切り出すメソッドって、親クラスである「AppController」の中に書いたほうが良いですね~
(「TopController」と「PdfController」の両方から呼び出せるように…)

A子

えっと…
その場合、アクセス修飾子は「private」ではなく「protected」でないとダメなんだっけ?

B美

おぉ、すごいじゃない

よく覚えてたわね

A子

まぁね(n*´ω`*n)

んじゃ、まずは「TopController」の「combinationResult」メソッドを書き換えるよ

//指定された数字を組み合わせて「ボックス」用の値を作成(結果画面)
public function combinationResult()
{
    if ($this->request->is('post')) {
        $box = [];

        //ボックス値の作成
        $digits = array_filter($this->request->getData('digits'), 'strlen'); //空文字列は削除(ただし、ゼロは除外)
        $box_set = $this->combination($digits, 3); //ボックスの組み合わせを作成
        foreach ($box_set as $row) {
            sort($row, SORT_NUMERIC);
            $candidate = ($row[0] * 100) + ($row[1] * 10) + ($row[2] * 1);
            array_push($box, sprintf("%03d", $candidate)); //ゼロパディングした3けたの文字列
        }

        $result = $this->doCombination($box);

        $this->set(compact('box', 'digits'));
        $this->set(compact('result'));
    }
}

「AppController」内にある「doCombination」メソッドを呼び出して結果を得るつもりだよ
あと、ビューにも$box及び$digitsを渡すことで、PDF作成のためのパラメータとして使う予定…

C菜

ではでは~
templates/Top/combination_result.php」については私のほうで追加しておきますね~

<div style="width: 100%;text-align: center;">
    <?= $this->Form->create(null, ['url' => ['controller' => 'pdf', 'action' => 'makepdf'], 'target' => '_blank']) ?>
        <?php foreach ($box as $idx => $val) { ?>
            <?= $this->Form->hidden("box.$idx", ['value' => $val]) ?>
        <?php } ?>
        <?php foreach ($digits as $idx => $val) { ?>
            <?= $this->Form->hidden("digits.$idx", ['value' => $val]) ?>
        <?php } ?>
        <?= $this->Form->button(__('PDFファイルを作成'), ['class' => 'button is-link']) ?>
    <?= $this->Form->end() ?>
</div>

ポイントとしては、フォーム作成時のオプションとして「'target' => '_blank'」を入れたことですね~
(別画面に表示したいので~)

あと、「"box.$idx"」と「"digits.$idx"」の部分は、勝手に「box[0]」や「digits[1]」って感じに書き換わりますよ~
(シングルクォートではなく、ダブルクォートで囲むのがポイントです~)

B美

さすがはC菜ね

シングルクォートだと「box0」や「digits1」になっちゃうのよね(苦笑)

A子

んじゃ、「AppController」の「doCombination」メソッドを書いてみよう
(というか、元々の「combinationResult」メソッドから切り出しただけ)

//各ボックス値についてDB検索及びソートを実行
protected function doCombination($box)
{
    $result = [];

    //各ボックス値の詳細情報を取得
    //直近の出現時期とその当選番号、過去の出現頻度等
    $connection = ConnectionManager::get('default');
    foreach ($box as $value) {
        //各桁の分解
        $digit = str_split($value);

        //データベース検索
        $sql =<<<EOF
            select lottery_time,lottery_date_dt,lottery_date_str, num3_str
              from numbers
              where least(num3_place100, num3_place10, num3_place1)={$digit[0]}
              and (num3_place100+num3_place10+num3_place1)-least(num3_place100, num3_place10, num3_place1)-greatest(num3_place100, num3_place10, num3_place1)={$digit[1]}
              and greatest(num3_place100, num3_place10, num3_place1)={$digit[2]}
              order by lottery_date_dt desc;
        EOF;
        $search_result = $connection->execute($sql)->fetchAll('assoc');

        //結果を格納
        if (count($search_result) > 0) {
            $recentry = $search_result[0]['lottery_date_str']; //直近の出現時期
            $lottery_time = $search_result[0]['lottery_time']; //直近の実施回
            $num3 = $search_result[0]['num3_str']; //直近のストレート数字
            $frequentry = count($search_result); //出現頻度(回数)
            array_push($result, [$value, $recentry, $lottery_time, $num3, $frequentry]);
        } else {
            array_push($result, [$value, '-', '-', '-', 0]);
        }
    }

    //直近の出現時期が古いものから順に並べ替え(ソートには「実施回」を使用)
    array_multisort(array_column($result, 2), SORT_ASC, $result);

    return $result;
}


C菜

「ConnectionManager」のuseを忘れないでくださいね~

use Cake\Datasource\ConnectionManager;

A子

おっと、そうだった…(苦笑)

B美

テスト実行した結果も問題ないみたいだし、これで準備は完了ね

ここからは「src/Controller/PdfController.php」に「makepdf」メソッドを書いていきましょう
まずは1番よ

1.フォーム送信された値で再計算


//「ボックス」の予想をPDF化
public function makepdf()
{
    $this->autoRender = false;

    if ($this->request->is('post')) {
        //ボックス値を取得し、再計算
        $box = $this->request->getData('box'); //ボックス値
        $digits = $this->request->getData('digits'); //候補数字
        sort($digits);
        $digit_cnt = count($digits);
        $result = $this->doCombination($box);
        $today = date('Y年n月j日 H:i:s');

        ・・・

    }
}

autoRender」をfalseにしている理由は、ビューファイルを使わないからよ

A子

候補数字($digits)をソートしたり、要素数を求めたりって何か意味あるの?

C菜

多分ですけど、PDF内に各種情報を表示する際に必要なんだと思いますよ~

B美

そういうこと

では、次は2番ね
(これが一番の問題なのよね)

2.Mpdfクラスをインスタンス化

A子

ん?
普通にnewするだけじゃないの?

B美

割と面倒なのよ(苦笑)
フォントディレクトリの取得やデフォルトフォントの取得なんかが必要になるからね

//フォントディレクトリ(デフォルト)
$defaultConfig = (new ConfigVariables())->getDefaults();
$fontDirs = $defaultConfig['fontDir'];

//フォント(デフォルト)
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontData = $defaultFontConfig['fontdata'];

//mPDFのインスタンス化
$mpdf = new Mpdf([
    'tempDir' => TMP.'mpdf',
    'fontDir' => array_merge($fontDirs, [ROOT.DS.'fonts']),
    'format' => 'A4',
    'orientation' => 'P',
    'fontdata' => $fontData + [
        'ipaexg' => [
            'R' => 'ipaexg.ttf'
        ],
        'ipaexm' => [
            'R' => 'ipaexm.ttf'
        ],
    ],
    'default_font' => 'ipaexg',
]);

かなり面倒っぽいけど、定型的な記述だと思っておけば良いから
(深く考えないように…)

C菜

えっと~
既存(デフォルト)のフォント情報(ディレクトリやフォント自体)に追加する形で、IPAフォントを設定するんですね~?

A子

面倒くさい!

IPAフォントだけを設定すれば良いじゃん

B美

うーん、多分それでも動くんだけどね

さっき見た「numapp/vendor/mpdf/mpdf/ttfonts」の中って、めっちゃたくさんのフォントが入っていたでしょ?
あれらが内部的に(こっそり)使われてたりするとヤバいかもしれない…ってことで「追加」が推奨されているのよ

A子

なるほどね
そういうことか

んじゃ、次は3番だね

3.CSSを定義(変数に格納)

C菜

CSSについては、tableタグの定義だけあれば良いんじゃないでしょうか~?

B美

そうね

変数$cssにヒアドキュメントで記述してみなさい

C菜

了解です~

あ、ついでに4番もやっちゃいますね~
($contentという変数に格納するです~)

4.HTMLによるコンテンツを定義(変数に格納)

//ヘッダー
$css =<<<EOF
table {
    border-collapse:collapse;
    width:80%;
    margin: 0 auto;
    font-size:12px;
}

table th{
    border:1px solid black;
    padding:10px 5px 10px 5px;
    text-align: center;
    background-color: #afffaf;
}

table td{
    border:1px solid black;
    padding:10px 5px 10px 5px;
    text-align: center;
}
EOF;

//ボディ
$content = '';
$content .=<<<EOF
<div style="width: 100%;text-align: center;">
    <h2>ボックス値(予想)一覧</h2>
    【{$today}に作成】
    <br /><br />
    <table>
        <tbody>
            <tr><th colspan="{$digit_cnt}">候補数字</th></tr>
            <tr>
EOF;
foreach ($digits as $val) {
    $content .= "<td>{$val}</td>";
}
$content .=<<<EOF
            </tr>
        </tbody>
    </table>
    <br /><br />
    <table>
        <tbody>
            <tr>
                <th>ボックス値</th><th>直近の出現時期</th><th>直近の当選番号<br />(ストレート)</th><th>このボックス値<br />の出現回数</th>
            </tr>
EOF;
foreach ($result as $val) {
    $content .= "<tr><td>{$val[0]}</td><td>{$val[1]}(第{$val[2]}回)</td><td>{$val[3]}</td><td>{$val[4]}回</td></tr>";
}
$content .=<<<EOF
        </tbody>
    </table>
</div>
EOF;

こんな感じでいかがでしょう~?


A子

いや、「いかがでしょう」って言われても、ブラウザで見てみないと分かんないよ(苦笑)

B美

そりゃ、そうね(笑)

C菜

次は5番です~

5.WriteHTMLメソッドの引数として(の変数)を渡したあと、PDFデータの書き出しを行う

B美

これは私のほうでやりましょう

//PDFへの書き出し
$mpdf->WriteHTML($css, HTMLParserMode::HEADER_CSS);
$mpdf->WriteHTML($content, HTMLParserMode::HTML_BODY);
$pdf = $mpdf->Output('', 'S');

これも(難しく考えず)定型的な記述だと思って良いわ

A子

いよいよ最後の6番だね

6.レスポンスを返す

てか、さっきの$pdfをreturnするだけじゃないの?

B美

そんな単純な話じゃないわ(苦笑)

まずはファイル保存する際のデフォルトのファイル名を決めましょう
(「numbers3box_年月日_時分秒.pdf」で良いかな?)

$fileName = 'numbers3box_'.date('Ymd_His').'.pdf';

で、レスポンス自体はこう書くの

return $this->response
    ->withType('pdf')
    ->withHeader(
        'Content-Disposition',
        'inline; filename="' . $fileName . '"'
    )
    ->withStringBody($pdf);

ちなみに、ヘッダーで「inline」指定しているのは(ダウンロードではなく)ブラウザ表示させたいからよ

C菜

それでは実行確認してみますね~

候補数字は適当に「5」「2」「6」「7」としてみました~
(特に意味はありませんけど~)





A子

おぉ!バッチリじゃん

ちなみに「保存」アイコンを押したときの(デフォルト)ファイル名もバッチリだったよ
(「numbers3box_20260613_091713.pdf」というファイル名だった)

C菜

終わってみれば案外簡単でしたね~
(もちろん、「mPDF」というPHPライブラリとB美部長の存在あってのことですけど~)