PHPでWordPressのカテゴリをSQLiteへコンバートする
「PHPでWordPressの投稿データをSQLiteへエクスポートする」という記事を投稿してからもう1年経ってしまいました。
格安サーバーではWordPressが重すぎなので、PHP+SQLiteで簡易CMSを作って移行したいと考え、まずはWordPressが使っているMySQLのデータをSQLiteへ移行するプログラムを作っていましたが、最近はあまり落ちていないようだったのですっかり放置していました。
さて、前回の記事でWordPressの投稿データは抽出できましたので、引き続きカテゴリの抽出をしていきたいと思います。
WordPressのカテゴリは3つのテーブルから成っている
WordPressはカテゴリとタグを一緒に管理しており、まとめて下記のテーブルに格納されています。
wp_terms | カテゴリやタグのIDと名称/スラッグを保持 |
wp_term_relationships | カテゴリIDと投稿データのIDを紐付けるテーブル |
wp_term_taxonomy | カテゴリやタグの分類およびツリー構造を保持 |
シンプルだった投稿データに比べると少し複雑ですね。
でも、カテゴリの一覧を取得するだけなら、こんなSQL文で良いかな?
SELECT * FROM wp_term_taxonomy INNER JOIN wp_terms
ON wp_term_taxonomy.term_id=wp_terms.term_id
WHERE taxonomy='category';
実際に全データをコンバートする際はタグも扱うつもりですが、長くなるので今回はカテゴリに絞ってコードを書いていこうと思います。
PHPでWordPressのカテゴリをツリー状に表示する例
MySQLからSQLiteへデータコンバートする前に、ぼくの理解で合っているのか確認する意味も含めてWordPressのカテゴリをツリー状に表示するコードを書いてみました。
//接続設定
$cfg['WP_PDO_DSN'] = 'mysql:host=localhost;charset=utf8mb4;port=3306;dbname=[データベース名];';
$cfg['WP_PDO_USER'] = '[MySQLユーザー名]';
$cfg['WP_PDO_PASS'] = '[パスワード]';
$cfg['WP_PDO_OPTION'] = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC);
//MySQL(MariaDB)へ接続
$db_wp = new PDO($cfg['WP_PDO_DSN'], $cfg['WP_PDO_USER'], $cfg['WP_PDO_PASS'], $cfg['WP_PDO_OPTION']);
viewCategory($db_wp);
//WordPressのカテゴリをツリー状に表示する関数
function viewCategory($db_wp, $parent = 0, $indent = 0)
{
//WordPressのwp_termsからcategoryのみを抽出
$query = "SELECT wp_term_taxonomy.*,wp_terms.name,wp_terms.slug".
" FROM wp_term_taxonomy INNER JOIN wp_terms ON wp_term_taxonomy.term_id=wp_terms.term_id".
" WHERE taxonomy='category'".
" AND parent=?".
" ORDER BY wp_terms.name";
$st = $db_wp->prepare($query);
$st->bindValue(1, $parent);
$st->execute();
while ($row = $st->fetch()) {
echo str_repeat('--', $indent); //階層の深さ分ハイフンを表示
echo $row['name'].'<br>';
//子階層を再帰呼び出し
viewCategory($db_wp, $row['term_id'], $indent + 1);
}
}
うちのブログのデータで試してみたところ…
PC関連 Tips VR Web運営 --CSS お金の話 ガジェット カメラ ゲーム トラブル事例 フリーソフト プログラミング --JavaScript --PHP --VBScript 未分類 生活 起業 電子書籍 音楽
と、このようにWordPressで表示されているとおりのカテゴリ名が取得できました。
ちょっと興味深かったのは、WordPressはカテゴリのソート順に関するパラメーターを持っていないようですね。
そんな馬鹿な…と思ってWordPressの管理画面を探しても見当たりません。まぁそもそもテーブルにそれらしい値がないですし…。じゃあどんな順番に並べてるんだろう、と思ったら(うちのブログでは)どうやらカテゴリ名の昇順っぽい? テーマなどでも変わってくるのかなぁ。
これで困ってる人いないのかなぁーと少しググってみたら、wp_term_taxonomyテーブルをいじってソート順を変更するプラグインなんかもあることがわかりました。
使用者が多いとこうして様々なプラグインが開発されるのは良いのですが、どんどん肥大化していくのも難点です。痛し痒し。
PHPでWordPressのカテゴリをSQLiteへ変換する
先程のカテゴリ表示でwp_term_taxonomyテーブルへの理解が間違っていないことがわかったので、あらためてMySQLからSQLiteへデータ移行したいと思います。
ただ、自前のCMSで使うテーブルですし、wp_termsとwp_term_taxonomyの2つに分ける必要なくない? と思ったので、カテゴリ自体はcategoryテーブル1本にしました。 (投稿データとの紐付けテーブルは別途用意)
■移行先(SQLite側)のカテゴリテーブル
id | INTEGER | PRIMARY KEYにするので自動採番 |
parent | INTEGER | 親カテゴリのID(トップは0) |
name | VARCHAR(200) | カテゴリ名 |
slug | VARCHAR(200) | スラッグ(URLで使われる名称) |
■コンバート用のコード
//WordPressのカテゴリをSQLiteへコンバートする関数
function convertCategory($db_wp, $db_sqlite)
{
//categoryテーブル作成
$query = 'CREATE TABLE IF NOT EXISTS category ('.
' id INTEGER PRIMARY KEY,'.
' parent INTEGER DEFAULT 0,'.
' name VARCHAR(200),'.
' slug VARCHAR(200))';
$db_sqlite->query($query);
//SQLiteへのINSERT文を準備
$st_sqlite = $db_sqlite->prepare('INSERT INTO category (id,parent,name,slug) VALUES (?,?,?,?)');
//WordPressのwp_termsからcategoryのみをコンバート
$query = "SELECT wp_term_taxonomy.*,wp_terms.name,wp_terms.slug".
" FROM wp_term_taxonomy INNER JOIN wp_terms ON wp_term_taxonomy.term_id=wp_terms.term_id".
" WHERE taxonomy='category'".
" ORDER BY wp_terms.name";
$st_wp = $db_wp->query($query);
$db_sqlite->beginTransaction();
while ($row_wp = $st_wp->fetch()) {
$st_sqlite->bindValue(1, $row_wp['term_taxonomy_id']);
$st_sqlite->bindValue(2, $row_wp['parent']);
$st_sqlite->bindValue(3, $row_wp['name']);
$st_sqlite->bindValue(4, $row_wp['slug']);
$st_sqlite->execute();
}
$db_sqlite->commit();
}
先に出したツリー状に表示するコードと異なり、parent(親ID)などもそのままセットするだけですから、特に複雑なところはないですね。
wp_term_taxonomyとwp_termsを結合してカテゴリIDと名称を持ったcategoryテーブルを作っているだけです。
MySQLからSQLiteへ持っていくからといってIDの振り直しなんて面倒なこともしていません。
だって、それをするとwp_term_relationshipsも書き換えなきゃいけなくなるし、デバッグするのも大変だもの…。
そんなんで良いのか、と思われるかもですが、SQLiteの自動採番は面白い仕組みで、「INTEGER PRIMARY KEY」で宣言された項目はINSERT時に最大値+1をセットされるので問題ありません。
これがPostgreSQLあたりだとシーケンス番号管理用のテーブルを別途用意して、そこをカウントアップしているため、こんな書き方だとIDの整合性が取れなくなり、新しいカテゴリを追加しようとした際にデュプリケーションエラーになるでしょう。
そのため、categoryテーブルへデータ移行した後、SELECT max(id)+1 FROM categoryで最大値+1の値を取ってきてsetval関数でシーケンス番号に最大値をセットしなおす、みたいな処理が必要となりますが、SQLiteでは必要ありません。
先に説明したとおり、「INTEGER PRIMARY KEY」で宣言された項目はSQLite側で自動採番してくれるため、このコンバートが終わった後に
INSERT category (name,slug) VALUES ('新しいカテゴリ', 'new');
を実行してもちゃーんと最大値+1のidをセットしてくれます。
たぶんMySQLも同じ仕組みなのでしょうね。PostgreSQL歴が長いので意外でした。
wp_term_relationshipsをSQLiteへ変換する
変換も何も…ただ値をセットしているだけですが、一応掲載しておきます。
//WordPressのwp_term_relationshipsをSQLiteへコンバートする関数
function convertPostCategory($db_wp, $db_sqlite)
{
//post_categoryテーブル作成
$query = 'CREATE TABLE IF NOT EXISTS post_category ('.
' post_id INTEGER,'.
' category_id INTEGER,'.
' PRIMARY KEY (post_id,category_id))';
$db_sqlite->query($query);
//SQLiteへのINSERT文を準備
$st_sqlite = $db_sqlite->prepare('INSERT INTO post_category (post_id,category_id) VALUES (?,?)');
//WordPressのwp_term_relationshipsをコンバート
$st_wp = $db_wp->query("SELECT * FROM wp_term_relationships");
$db_sqlite->beginTransaction();
while ($row_wp = $st_wp->fetch()) {
$st_sqlite->bindValue(1, $row_wp['object_id']);
$st_sqlite->bindValue(2, $row_wp['term_taxonomy_id']);
$st_sqlite->execute();
}
$db_sqlite->commit();
}
wp_term_relationshipsじゃ個人的にわかりづらいので、post_categoryというテーブルにしています。
post_idとcategory_idを紐付けるだけのテーブルですね。
あーでも、タグも変換するなら一緒にしたほうが良いのかなぁ。でもカテゴリとタグは別テーブルのほうがわかりやすい気もするし、ちょっと悩む…。
まとめ
- WordPressのカテゴリはwp_terms/wp_term_relationships/wp_term_taxonomyの3つで構成されていた。
- カテゴリをツリー表示するだけならwp_term_taxonomyとwp_termsを結合して再帰呼び出しすればOK。
- 自前CMSへ移行するとしたらシンプルなほうが良いので、wp_term_taxonomyとwp_termsを結合させたcategoryテーブルを用意することにした。
- wp_term_relationshipsは投稿データとカテゴリの紐付けなのでそのまま持ってくることにした。
といったところでしょうか。
ちょっと雑ですが、これでとりあえずは投稿データとカテゴリの移行が完了したので、それっぽいページは作れるんじゃないですかね。
昔のMovableTypeみたいにSQLiteからデータ抜き出してHTMLを生成しちゃうとか。file_put_contentsだけで作れるでしょう。
ただ、あの仕組みはちょっと共通部分変更しただけで全部の記事を再生成しなきゃいけないので微妙なんですよねー。表示は軽くなるけど、デメリットも大きすぎる。
それならキャッシュファイルとしてコンテンツ部分だけファイルに書き出しておくという手もあるし、そんなことしなくてもSQLiteから読みだしたほうが早いでしょ、という考え方もあるし…………うん、このあたりはまたのんびり考えていきましょう。