Movable Type拡張プラグラミングに関する10の良い習慣。
公開日 : 2007-07-03 12:22:43
昨日改めて公開したMovable Type RebuildAt1stView(Beta) のことを当初「Movable Type用「逐次再構築(仮称)」プラグイン+α(β版)」と表現したわけですが、実際「プラグイン(.pl)」だけではなくて様々なファイルで構成されています。以前にも何度か書きましたが「Movable Typeを開発プラットフォーム」であると捉えれば「プラグイン」以外にも様々な拡張方法があります。
参考エントリー
これまでに色々やって来たまとめという意味もありウチのスタッフへの説明の意味も込めて「高いメンテナンス性を保ちながらMovable Typeを本格的に拡張するためのヒント」について少しまとめておきたいと思います。
「良い習慣」と書きましたが、受託ベースのWeb屋の思考回路での考え方です。もちろんオープンソース(GPL)版をベースにコミュニティベースで開発を進める場合にもなんらかのルールは必要だと思いますし、そのあたりを考える意味もあって書いてみました。
このエントリーは以下のプラグインを例にとって書かれています。
- Movable Type RebuildAt1stView(Beta)。(エントリー)
- RebuildAt1stView.zip(16Kb)(ダウンロード)
MT3/4両対応。エントリーアーカイブへの最初のアクセスがあった時点で再構築(静的ファイル生成)を行うMovable Type用のプラグイン, CGIスクリプト等で構成されています。
RebuildAt1stView(Beta) の構成
参考ページ
さて、ここからが本編となります。
1.出来る限りイリーガルなDB利用は避ける。素直に新しいテーブルを作る。
『このサイトでは「コメント」は受け付けないので「mt_comment」テーブルを別の用途に使おう』、とか『「created_byとmodified_byは、現在使われていません。」とあるのでこの2つは別の用途に使えるじゃん! 』みたいなイリーガルな使い方(本来のDBの設計の考え方から外れた使い方)を僕自身もしていた時期がありましたが、MTのバージョンアップ時にデータベース構造が変更になったり以前のバージョンでは未使用だったフィールドをMTが使うようになったり、あるいは別のプラグインで同じような考え方をしていた場合(例えばcreated_byとmodified_byを使おう、というプラグイン作者が他にもいた場合)当然コンフリクトがおこってしまいます。
MTには新しいテーブルを作ってアクセスする方法が用意されています。MT::Objectのサブクラスを作る方法です。この方法で拡張した場合、MTの他のテーブルと同じようにSQLを意識せずに読み書き検索などが可能です。
今回のプラグラムでは PerlmalinkからEntryのidを割り出す必要があって、これはもちろん可能なわけですが(例えばこちらのプラグインではその処理を行うようになっています)検索に時間がかかったり処理に負荷がかかっては意味がないわけです。何ぶん動的生成・静的生成の良いところをとってできるだけ負荷が少なく作成者にも閲覧者にもサーバーにもメリットがある方法を探ろう、という意図で作ったものだからです。 そこで、perlmalinkとentry_idを紐付けるだけのmt_permalinkというテーブルをデータベースに追加することにしました。
permalink_id | int(11) |
---|---|
permalink_blog_id | int(11) |
permalink_permalink | varchar(255) |
permalink_modified_on | timestamp |
データベースをさわる関係でインストールや設定が大変という面はありますが、素直にMT::Objectのサブクラスを作って拡張した方が後々悩まずに済みますし、フィールド名もわかりやすく設定できますからコーディング時にもわかりやすくなります。
2.MT::Objectのサブクラスを定義した.pmファイルは/plugins/foo/lib/MT/以下に置く。
「mtフォルダのlib/以下やextlib/以下にfoo.pmを設置してください」というタイプのプラグインやソリューションがありますが、インストールの箇所が複数になるのはいかにも面倒ですし、削除する時に忘れてしまう可能性もあります。インストールが面倒だと開発途中で自分自身が面倒です。/plugins/foo/lib/MT/以下に設置しておけばプラグイン/plugins/foo/foo.plからはパスが通っていますのでフォルダ丸ごと上書きアップロードでインストールやバージョンアップが可能です(もちろんアンインストールもフォルダを削除するだけです)。
3.Transformerプラグインを利用したCMSのテンプレート書き換えは最小限に留める。
CMSに新しいボタンを追加したり管理画面を拡張するTransformerプラグインはいかにも「派手」なのでついついテンプレートをぐちゃぐちゃと書き換えることに楽しみを見いだしてしまいがち(違?)ですが、以前も書きましたがCMSのテンプレートを正規表現で検索してバギバキ置換して書き換えるのは極力避けた方が良いと思っています。バージョンアップでテンプレートが変更になった時にすぐ動かなくなりますし、これも他のプラグインとコンフリクトを起こす可能性があります。
但し、V4からはDOMがサポートされたようですので、これまでのように数文字変更されただけで正規表現にかからなくなってTransformerプラグインが動かなくなるということはないようです。
以下「Movable Type v4.0 (Athena): A Developer's Perspective (Movalog: Movable Type Tips & Tricks)」より
これまでは粗雑な検索に頼って必要なコードの行を捜し求めて、それを私たちが欲しかったものに置き換えるテクニックを利用してきた。この方法には少しの変化でプラグインが動かなくなる問題があった。
V4.0では、一連のDOMのような方法を導入した(getElementById、getElementsByTagNameなどを含んでいる) 。
これらのメソッドは、私たちが(APP)CMSテンプレートの中から簡単にフィールドやとマークアップを見つけることを助けるだろう。
これはTransformer pluginsがより「壊れにくく」なったことを意味する。
これがどのように役立つかについての素晴らしい例が「テンプレート編集画面」にある。 新しくなった4.0のテンプレート編集画面ではテンプレートがMTIncludeタグを含むとき、テンプレートへのリンクがサイドバーで自動的に表示される。
CMSのテンプレートをカスタマイズする最も簡単な方法は、mt/alt-tmpl/以下にtmpl/cms以下のテンプレートを(必要なものだけ)コピーしてコピーの方をカスタマイズする方法です。この方法のメリットは「ノンプログラミング」でカスタマイズが可能なことです。「抜粋(概要)」を「スペック」に書き換える、といった作業であればHTMLコーディング程度の知識でカスタマイズできます。
但しこの方法もコンフリクトやバージョンアップの際の問題は避けられません。また、設置場所が複数に分散するのでインストールや更新が煩雑になる問題も解決出来ません。
やはりTransformerプラグインは(コードの保守性の面からは)最小限にすべきかと思います。
4.既存テンプレート書き換えでなく「__mode」と新規テンプレートを追加する。テンプレートはプラグインフォルダ内/tmplフォルダに置く。
ではどうするか。ケースバイケースですが、単に新しい画面を用意するだけであればこの場合のグッド・ノウハウは『新しいアプリケーションのモード(__mode)を作って「プラグインアクションから」呼び出す。CMSのテンプレートは新しく用意して/plugins/foo/以下に置く』というものです。/plugins/foo/以下に置くのは前項と同じくインストールや更新を楽にするためです。今回は/plugins/RebuildAt1stView/tmpl/ 以下に置いています(V3.tmpl及びV4.tmpl)。
新しい__modeを定義する
app_methods => {
'MT::App::CMS' => {
'remove_all_entry_archive' => ¥&_remove_all_entry_archive,
'update_permalink' => ¥&_update_permalink
},
},
以下のように呼び出すことができます。
mt.cgi?__mode= remove_all_entry_archive&...
今回作成したのは、「ボタンをクリックするとmt_entryから各エントリーのid,permalink,modified_onを取得してmt_permalinkに登録する(初期化)ための機能と」「ボタンをクリックすると静的に生成されたエントリーアーカイブファイルを全消去するもの」です。これらの機能のためには「ボタンを追加する」「実行結果をユーザーにフィードバックする」といった(各々)2種類のテンプレートが必要です。
5.(ユーザビリティにこだわるところでなければ, )ボタンの追加には「add_plugin_action」または「config_link」を使う。
逆に言えば, CMSのユーザビリティにこだわるところではTransformerプラグインで書く、ということになります。MTの拡張の場合はユーザービリティとコードのメンテナンス性がトレードオフの関係になる場合があることを意識しておいた方が良いと思います。
ボタンを追加するだけであればテンプレートのカスタマイズは不要です。管理画面のユーザビリティを向上するためには「ボタンの位置」等が大切ですが、メンテナンス性とコードの可読性を優先するのであれば「add_plugin_action」または「config_link」を使うと良いでしょう。
config_link => '../../'.MT->config('AdminScript').'?__mode=update_permalink'
あるいは
MT->add_plugin_action( 'blog',
'../../'.MT->config('AdminScript').'?__mode=remove_all_entry_archive',
$plugin->translate('Remove All Entry Cache')
);
オープンソース(GPLバージョン)をベースにコミュニティで開発する場合等は__modeの命名規則等を定めておいた方が良いと思います。
6.MT3.xとMT4のバージョン判別・処理切り分けはできるだけシンプルにする。
MT3.xとMT4では管理画面のデザインが大幅に異なっていますので、さすがにここは共有できませんが、単に実行結果を表示するだけであれば非常にシンプルなテンプレートを用意すれば済みます。
plugins/RebuildAt1stView/tmpl/V4.tmpl
<$mt:include name="include/header.tmpl"$>
<TMPL_VAR NAME=DIV><a href="javascript:void(0)" onclick="javascript:hide('saved');" class="close-me"><span>close</span></a>
<TMPL_VAR NAME=CONTENT>
</div>
<$mt:include name="include/footer.tmpl"$>
plugins/RebuildAt1stView/tmpl/V3.tmpl
<TMPL_INCLUDE NAME="header.tmpl">
<h2><span class="weblog-title-highlight"><TMPL_VAR NAME=PAGE_TITLE ESCAPE=HTML></h2>
<TMPL_VAR NAME=DIV><TMPL_VAR NAME=CONTENT></div>
<TMPL_INCLUDE NAME="footer.tmpl">
plugins/RebuildAt1stView/RebuildAt1stView.pl (プラグイン本体の抜粋)
# バージョン判別
my $mt_version;
my $transform_tmpl;
my $div_success;
my $div_error;
my $v = MT->version_id;
#->4.0 Beta 1
# バージョン毎にCMSテンプレート関連の定義
if ($v =~ /^4/) {
$mt_version = 40;
$transform_tmpl = 'V4.tmpl';
$div_success = '<div id="saved" class="msg msg-success">';
$div_error = '<div id="generic-error" class="msg msg-error">';
} else {
$mt_version = 30;
$transform_tmpl = 'V3.tmpl';
$div_success = '<div class="message">';
$div_error = '<div class="error-message">';
}
(中略)
sub _update_permalink {
my $app = shift;
# プラグインから呼び出すテンプレートの設置場所のパスを指定
$app->{plugin_template_path} = 'plugins/RebuildAt1stView/tmpl';
my $user = $app->user;
my %param;
$param{page_title} = $plugin->translate('Update "mt_permalink"');
if ($user->is_superuser) {
MT::Permalink->remove_all;
(中略)
$param{div} = $div_success;
$param{content} = $plugin->translate('Entry Archives List was Updated.');
return $app->build_page($transform_tmpl, ¥%param);
(以下略)
MT4.0での結果表示
MT3.3での結果表示
7.ログインなしで動作させる箇所やアドレスを晒したくない場合、あるいは軽快さを求める箇所ではMT::Bootstrapを利用する。
__modeで新しい動作を定義する方法もありますが、コメントやトラックバック等は「ログインしていないユーザー」が利用するわけです。こういったケースではMT::Bootstrapを利用するの良いでしょう。RebuildAt1stViewではmtview.cgiからMT::Bootstrapを利用してMTViewCGI.pmを走らせています。MT::Bootstrap を利用したアプリケーションは (MT3.34以降であれば) そのままFastCGIに対応できるというメリットもあります。またmt.cgiのパスを晒さなくて済みます(例えばApache側でmt.cgiのアクセスを制限すればセキュリティも向上させられます)。
plugins/RebuildAt1stView/mtview.cgi (あるいはmtview.fcgi)
#!/usr/bin/perl -w
use strict;
use lib 'lib';
use lib '../../lib';
use lib '../../extlib';
use MT::Bootstrap App => 'MTViewCGI';
plugins/RebuildAt1stView/lib/MTViewCGI.pm
package MTViewCGI;
use strict;
use MT;
use MT::App;
(中略)
use MT::Permalink;
@MTViewCGI::ISA = qw(MT::App);
sub init_request {
my $app = shift;
$app->SUPER::init_request(@_);
$app->add_methods( MTViewCGI => ¥&_MTViewCGI );
$app->{default_mode} = 'MTViewCGI';
$app->{requires_login} = 0;
$app;
}
my $mt = MT->new(Config => '../../mt-config.cgi');
my $plugin = new MT::Plugin::RebuildAt1stView({ name => 'RebuildAt1stView' });
# プラグインの設定値を得る
my $base_url = $plugin->get_config_value('base_url');
my $base_pth = $plugin->get_config_value('base_pth');
my $error_tmpl = $plugin->get_config_value('error_tmpl');
sub _MTViewCGI {
my $app = shift;
# ここに処理内容を書く
8.設置調整時にできるだけソースに手を入れなくて良いように設定項目は管理画面から行わせる。
「環境にあわせてプログラムのパスを修正」とかやると導入の敷居が一気に高くなってしまいます。ただでさえプログラム未経験者は呪文の固まりのようなプログラムに手を入れるのを嫌がります。趣味のプログラムなら良いでしょうが、多くの人に使っていただくあるいは仕事で客先に納入するものであれば極力プログラムソースに手を入れる必要がないようにすべきでしょう。
プラグインの設定画面のテンプレートはHTML::Templateに則った形式/拡張子を.tmplとしてをplugin/foo/tmpl/以下に置きます。
今回のケースでは plugins/RebuildAt1stView/tmpl/sys_config.tmpl, plugins/RebuildAt1stView/tmpl/blog_config.tmplの2種類。
9.Blog毎にプラグインの有効・無効を設定するために blog_config_template を用意する。
標準ではブログ毎にプラグインを有効・無効にすることはできません(と思う、多分)。プラグインを有効にするとすべてのブログが動作対象になってしまうので、ブログ毎にプラグインの有効・無効を指定したい場合はプラグイン設定テンプレートにチェックボックスを一つ追加してこの値で分岐させます。
plugins/RebuildAt1stView/tmpl/blog_config.tmpl
<div class="setting">
<div class="label">
<label for="rebuild_at_fview"><MT_TRANS phrase="Enable plugin:"></label>
</div>
<div class="field">
<p><label><input value="1" type="checkbox" name="rebuild_at_fview" id="rebuild_at_fview" <TMPL_IF NAME=REBUILD_AT_FVIEW>checked="checked"</TMPL_IF> />
<MT_TRANS phrase="Plugin Active."></p></label>
</div>
</div>
plugins/RebuildAt1stView/RebuildAt1stView.pl (プラグイン本体/ブログ毎のON/OFFの判別部分)
sub _post_save_entry {
my ($eh, $app, $entry, $original) = @_;
if (&plugin_active($app)) {
# ...プラグインが有効なら処理する
# プラグインの有効・無効を判別するルーチン
sub plugin_active {
my $app = shift;
my $blog = $app->blog;
my $blog_id = $blog->id;
my $get_from = 'blog:'.$blog_id;
return $plugin->get_config_value('rebuild_at_fview', $get_from);
}
10.日本語版だけで良くても国際化対応させる。
何故なら, こうすることでUTF-8とかEUCとか気にしなくて良くなるからです。
ということで、このあたりを反映させたプラグインの登録(設定)部分のコードは以下のような感じになります。
plugins/RebuildAt1stView/RebuildAt1stView.pl (プラグイン本体/設定関連)
my $plugin = new MT::Plugin::RebuildAt1stView({
name => 'RebuildAt1stView',
version => '0.5',
author_name => 'Junnama Noda',
author_link => '/online/',
description => '<MT_TRANS phrase=¥'_PLUGIN_DESCRIPTION¥'>',
app_methods => {
'MT::App::CMS' => {
'remove_all_entry_archive' => ¥&_remove_all_entry_archive,
'update_permalink' => ¥&_update_permalink
},
},
settings => new MT::PluginSettings([
['rebuild_at_fview', { Default => 1 }],
['base_url'],['base_pth'],['error_tmpl']
]),
# ↑PluginDataに保存する設定項目
system_config_template => 'sys_config.tmpl',
# ↑システム全体のプラグイン設定
blog_config_template => 'blog_config.tmpl',
# ↑ブログ毎の全体のプラグイン設定
l10n_class => 'RebuildAt1stView::L10N',
# ↑国際化対応
config_link => '../../'.MT->config('AdminScript').'?__mode=update_permalink'
});