CakePHP 4 で Markdown を HTML に変換し behavior を使って効率的に保存するアイディア

はじめに

先日投稿した「CakePHP 4 で Parsedown を使い Markdown をパースして HTML に変換する方法」では Markdown を HTML に変換する方法をご紹介しました。

今日はその応用で、Markdown で入力したテキストをデータベースに保存する際に、併せて HTML 化したものも自動的に保存する方法をご紹介します。

今回も Markdown の HTML 変換に Parsedown ライブラリを使っています。
また HTML の圧縮(ミニファイ)には HtmlCompress を使用しています。

CakePHP
4.0.3
Parsedown
1.7.4
HtmlCompress
3.0.0
目次
  1. Parsedown をインストール
  2. DB準備、モデルとエンティティを作成
  3. 記事登録フォームと確認用画面を作成
  4. Behavior で HTML 自動保存
  5. 保存時に HTML を圧縮して軽量化
  6. おわりに

1. Parsedown インストール

Parsedown の README に沿って Composer を使ってインストールします。

> cd \path\to\cakephp4
> composer require erusev/parsedown

2. DB準備、モデルとエンティティを作成

今回使用したデータベースの SQL です。データは後で作成するフォームを使って追加します。

CREATE TABLE `articles` (
  `id` int(11) NOT NULL,
  `body` text NOT NULL,
  `body_html` text NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

ALTER TABLE `articles`
  ADD PRIMARY KEY (`id`),
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

データベースの準備が完了したら Bake コマンドでモデルとエンティティを作成します。

> cd \path\to\cakephp4
> bin\cake bake model articles --no-test --no-fixture
--no-test --no-fixture はテスト用のファイルを作らないためのオプションです

3. 記事登録フォームと確認用画面を作成

動作確認で使う記事登録フォームと保存内容の表示画面を作ります。
今回は一つの画面に両方実装しちゃいます。

/src/Controller/SampleController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class SampleController extends AppController
{
  public function index()
  {
    $this->loadModel('Articles');
    // 新規登録
    if ($this->request->is('post')) {
      $newArticle = $this->Articles->newEntity($this->request->getData());
      $this->Articles->save($newArticle);
    }
    // 最新の記事を1件取得
    $article = $this->Articles->find()
      ->order(['id'=> 'DESC'])
      ->first();
    $this->set(compact('article'));
  }
}
/templates/Sample/index.php
<form method="post">
  <div>
    <textarea name="body"></textarea><br>
    <button type="submit">登録</button>
    <input
      type="hidden" name="_csrfToken" autocomplete="off"
      value="<?= $this->request->getAttribute('csrfToken') ?>">
  </div>
</form>

<?php if ($article) : ?>
  <!-- マークダウン -->
  <pre><?= $article->body ?></pre>
  <!-- HTML -->
  <pre><?= h($article->body_html) ?></pre>
<?php endif; ?>

4. Behavior で HTML 自動保存

記事情報を保存する際に Markdown テキストを HTML に変換して登録する機能を追加します。今回の例では articles だけですが、他のモデルでも同機能を簡単に実装できるように、Behavior として実装します。

/src/Model/Behavior/MarkdownBehavior.php
<?php
declare(strict_types=1);

namespace App\Model\Behavior;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Parsedown;

class MarkdownBehavior extends Behavior
{
  protected $_defaultConfig = [
    'fields' => []
  ];

  public function beforeSave(Event $event, EntityInterface $entity, $options)
  {
    $Parsedown = new Parsedown();
    $fields = $this->getConfig('fields');
    // $srcField が Markdown のカラム名、
    // $saveField が変換した HTML を保存するカラム名
    foreach ($fields as $srcField => $saveField) {
      // Markdown テキスト
      $mdText = $entity->get($srcField);
      // HTML に変換して Entity の $saveField プロパティに追加
      $entity->set($saveField, $Parsedown->text($mdText));
    }
  }
}
/src/Model/Table/ArticleTable.php
<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class ArticlesTable extends Table
{
  public function initialize(array $config): void
  {
    parent::initialize($config);

    $this->setTable('articles');
    $this->setDisplayField('id');
    $this->setPrimaryKey('id');

    // ▼追加
    $this->addBehavior('Markdown', [
      'fields' => [
        'body' => 'body_html'
      ]
    ]);

    $this->addBehavior('Timestamp');
  }
}

今回は body と body_html だけですが、汎用性を高めるために複数フィールドに対応させています。例えば articles テーブルに、abstract、abstract_html フィールドもある場合には、下記のように追加するだけで OK です。

/src/Model/Table/ArticleTable.php
$this->addBehavior('Markdown', [
  'fields' => [
    'abstract' => 'abstract_html' // ←この1行を追加
    'body' => 'body_html'
  ]
]);

5. 保存時に HTML を圧縮して軽量化

容量を少しでも軽くするためには HTML の圧縮も有効です。今回は HtmlCompress ライブラリを使いました。

以前に「CakePHP 3.8 に CakePHP Minify HTML Plugin を導入」で紹介した CakePHP Minify HTML Pluginライブラリでも使われているものです。

HtmlCompress も README.md にある通り composer でインストールします。

> cd \path\to\cakephp4
> composer require wyrihaximus/html-compress

HTML 圧縮導入のサンプルコードは以下の通りです。

/src/Model/Behavior/MarkdownBehavior.php
<?php
...
use WyriHaximus\HtmlCompress;

class MarkdownBehavior extends Behavior
{
  ...
  public function beforeSave(Event $event, Article $entity, \ArrayObject $options)
  {
    $Parsedown = new Parsedown();
    $fields = $this->getConfig('fields');
    // $srcField が Markdown のカラム名、
    // $saveField が変換した HTML を保存するカラム名
    foreach ($fields as $srcField => $saveField) {
      // ▼ここの処理を変更
      // Markdown から HTML に変換
      $mdText = $entity->get($srcField);
      $sourceHtml = $Parsedown->text($mdText);
      // HTML を圧縮して $saveField プロパティにセット
      $parser = HtmlCompress\Factory::construct();
      $compressedHtml = $parser->compress($sourceHtml);
      $entity->set($saveField, $compressedHtml);
    }
  }
}
use WyriHaximus\HtmlCompress\Factory; にしていないのは、
呼び出すときに Factory::construct() だと抽象的で分かりづらいと考えたためです。

6. おわりに

前回の方法だと表示時に毎回 Markdown テキストを HTML に変換することになりますが、今回ご紹介した方法を使えば、その処理を保存時のみに減らすことができます。

しかし、テーブルのフィールド数と容量は増えることになるので、どちらを取るか、という話にもなってきますね。

また、今回は保存直前に少しでも容量を減らすために HTML を圧縮しました。これが良いかどうかもケースバイケースだとは思いますが、必要な機能を実装して終わりではなく、「より良くする方法はないか?」を考えるのは大切なことだと思います。