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 インストール
Parsedown の README に沿って Composer を使ってインストールします。
> cd \path\to\cakephp4
> composer require erusev/parsedown
- Installation (GitHub - erusev/parsedown: Better Markdown Parser in PHP)
- https://github.com/erusev/parsedown#installation
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
3. 記事登録フォームと確認用画面を作成
動作確認で使う記事登録フォームと保存内容の表示画面を作ります。
今回は一つの画面に両方実装しちゃいます。
<?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'));
}
}
<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 として実装します。
<?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));
}
}
}
<?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 です。
$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ライブラリでも使われているものです。
- GitHub - WyriHaximus/HtmlCompress
- https://github.com/WyriHaximus/HtmlCompress
- GitHub - WyriHaximus/MinifyHtml: CakePHP Minify HTML Plugin
- https://github.com/WyriHaximus/MinifyHtml
HtmlCompress も README.md にある通り composer でインストールします。
> cd \path\to\cakephp4
> composer require wyrihaximus/html-compress
HTML 圧縮導入のサンプルコードは以下の通りです。
<?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);
}
}
}
呼び出すときに Factory::construct() だと抽象的で分かりづらいと考えたためです。
6. おわりに
前回の方法だと表示時に毎回 Markdown テキストを HTML に変換することになりますが、今回ご紹介した方法を使えば、その処理を保存時のみに減らすことができます。
しかし、テーブルのフィールド数と容量は増えることになるので、どちらを取るか、という話にもなってきますね。
また、今回は保存直前に少しでも容量を減らすために HTML を圧縮しました。これが良いかどうかもケースバイケースだとは思いますが、必要な機能を実装して終わりではなく、「より良くする方法はないか?」を考えるのは大切なことだと思います。