プログラミングの最近のブログ記事

DynamicMTMLは一言でいえば「静的なHTMLファイルの中のMTタグをダイナミックに処理する」ということになるでしょう(HTMLじゃなくてもXMLでもJavaScriptでも何でもいいわけですが)。確かにそのことによってスマートフォンに対する分岐なんかがMTタグで書けるようになったわけです。

ただ、それはDynamicMTMLの一面にすぎません。ただ、一度に紹介しきれないので何度かに分けてご紹介しようと思います。

MTPluginクラスとconfig.php

そうなんです。元々MTのダイナミックパブリッシングはSmartyの拡張なので、Smartyの作法に従って function.mthellomtworld.php みたいなファイルをタグの数だけ用意する必要があります。MTのphp/libディレクトリの下を覗いてみてください。いくつファイルありますか? 正直やってらんねーな、というほどのファイル数です(よくメンテしてるな、てか、よく書いたなこれ)。

DynamicMTMLにはMTPluginというクラスがあって、そいつを継承したクラスを作ることでこんな書き方ができるようになります。ファイルの位置は plugins/HelloWorld/php/config.php です。配列の形で指定するから、ひとつのファイルにまとめてタグを実装できます。案件で使う処理をまとめて書けるからメンテンスもしやすいですよね。

<?php
class HelloWorld extends MTPlugin {
    var $app;
    var $registry = array(
        'name' => 'HelloWorld',
        'tags' => array(
            'function'
                => array( 'helloworld'
                    => 'hdlr_helloworld' ),
        ),
    );
    function hdlr_helloworld($args, &$ctx) {
        return 'Hello MT World.';
    }
}
?>

MT使いの方ならこのコード、ピンとくると思うんですが、そう、Perlでの書き方と殆ど同じですよね。Perlのinit_registryと同じ指定方法です。

この例はテンプレートタグの書き方ですが「Perlと同じ書き方」ができるようにDynamicMTMLにはMT::Objectと'ほぼ'互換のORマッパやMTのタスク(run-periodic-tasksとほぼ同等のことがPHPでかけたり)や、静的ファイルの再構築をPHPで行う方法なんかも含まれています。

次回以降改めてご紹介しますが、僕にしては珍しく? ドキュメントも丁寧に書いているので、宜しければ参考にしてください。

リクリのセミナーの懇親会でこういう話(変数と関数をプログラム未経験者にに説明するには?)が出たんですよ。今度そういうことを教えることになったという方と話していたので。そこで考えたこと。

"箱"っていうんですよね皆さん。そう本に書いてあったって。僕も見た記憶があります。でも何かピンと来ないんですよね。そこでこんな話をしました。

「例の件どうなった?」の「例の件」って、会話の流れ(コンテキスト)によって違うもの指すよね?

例えば「この案件」でもいいです。「会社のfacebookのファンページ作る件だけど」ってのが、「変数に入れる」ということ。以降の話で「この案件」は「会社のfacebookのファンページ作る件」を差しますよね。そして、会話の流れによって「この案件」が差すものは変わります。

で、「案件の納期を調べる」というのが関数(サブルーチンとかファンクションとか)で、その中身は

「Backlogにログインして案件名で検索して納期の記述を調べる」

ということ。

そうすることで、

「"この案件"の納期を調べる」が「Backlogにログインして「会社のfacebookのファンページ作る件」の納期を調べる」という実行結果を返すようになる、と。

どうだろう? ちょっとは分かりやすいだろうか? 一般向けだとBacklogより"共有フォルダのExcelファイルを検索する"とかのほうがわかりやすいかもしれないですが。


一応(このエントリの話題と関係ないけど)、当日のセッションのスライドを貼っておきます。

第2回 CakePHP2.0 勉強会@Tokyoに参加してLightningTalkしてきました。CakePHPのブログチュートリアルを途中までしかさわったことのないなんちゃってCakePHPユーザーにもかかわらず...

実際のところ激しく道に迷って前半聞けてません。後半も細かいことはわかんなかったですが、CakePHPコミュニティの雰囲気はわかりましたよ!

※しかし個人で数百万円立て替えてお祭り主催ってすごいなPHP!

本当はこっち(デザイナー向けCakePHP勉強会 : ATND)で話した方が良いネタな気もしましたが、また機会があればということで(Ustは見ていたのでした)。

LightningTalkのお題はMTCake。CakePHPのViewをMovable Typeのタグ(MTML)で書けるようになるプラグインです。

CakePHPコミュニティとMTコミュニティの話とテンプレートエンジンとMVCの話

かたやフレームワーク、かたやソフトウェアなので違いがあって当然ですがCakePHP2.0勉強会の参加者は開発者(プログラマ)中心です。MTの方はデザイナ/フロントエンドエンジニア中心。プログラマやプラグイン作成社の方が少ないです。とはいえMTってのはテンプレートエンジンやアプリケーション開発のフレームワークを内包していて、開発系の勉強会をすることもできます。LightningTalkした時の反応が面白かったのが「コントローラーとビューは完全に別れています(一部MTMLでロジックも書けるけど)」といった時の反応(そうだよねそうあるべきだよね / それはそれでどうかと思うけどの両方の反応がありました)。

CakePHPの勉強会では必ず(1回参加+Uset鑑賞一回のくせに必ずとか言うなw)MVCの話が出ます。MTの勉強会ではMVCとか話題に出ません(僕が何度かセミナーなんかで話したことはあるけども)。MTの勉強会ではMVCとか概念の話は出ません。いきなりこう書けば、こう出る。 <MTEntryTitle> と書けばブログ記事のタイトルが表示されます、という話から始まって MTIfとかブロックのループとかを話しちゃってます。

これは、Movable Typeのそもそもの由来というか(10周年ですね)プログラマとデザイナという夫婦の会話が関係しています(いや、知らないけど。以下、フィクション)。

いま作っているブログツールなんだけど、テンプレートエンジンをどうしようか迷っているんだ。MVCっていう考え方に...
MVC? MVPならわかるけど野球の話? うーん、エンジンってそもそも分かんないわ車の話とか...テンプレートはわかるわ私ちゃんとテンプレートくらいは用意してデザイン進めるもの。
HTMLはわかるよね。
ばかにしないでよちゃんと勉強したわ。
<TMPL_VAR NAME=ENTRYTITLE>で書いたファイルをFTPでサーバーにアップするってのはどうだい?
そんな呪文書いたファイルをFTPとか...できればFTPも使いたくないわ。あと、EntryTitleのほうが見やすいし、HTMLだったら大文字小文字どっちでもちゃんとブラウザで表示されるし、そんな簡単なのがいいわ。

※ここでHTML::Templateを使うのをあきらめる

オーケイ、テンプレートエンジン、いや、エンジンじゃないな、HTMLみたいなタグで扱えるようにしよう。タグは、えー、そう、君の名前の頭文字をとってMTからはじまるようにしよう!
素敵!

という会話があったかどうか僕には知る由もないが、作ったものをデザイナーに教えるというよりもデザイナーとの共同作業の中で生まれたしくみやルールだからこそこれだけ普及したしデザイナー/フロントエンジニア人口が多いんだろうと思ったり。

特に今のCakePHPの仕組みが良いとか悪いとかMTMLがベストとか思わないけど、MTCake面白いので誰か一緒にやってみませんか? というメッセージを伝えにいったのでした。伝わったかな?

えー、まぁ、何というかセキュリティアップデート続きのおかげもあって今日も仕事してます。ええ。最初は行く予定だったんですけどね。

さて、ちょっと切り替えて真面目に行きます。

仮にテスト・ドリブンの開発を行っていたとしても、バグやセキュリティ脆弱性が見つかることはある程度以上のソフトウェアにとっては現状のところ避けられない問題ではないかと思います。ソフトウェアのコードを読み進むことで開発者が想定していない問題が見つかったりすることは実際にある訳で、最終的にはコードレビューこそが最終的な品質管理だと言えるのではないでしょうか(結局のところ開発者の予期しなかった問題を指摘するのはアナログな手法でコードを読めるテスターの存在頼りだったりします)。

現在、MT開発チームはセキュリティ強化月間らしいです。コードレビューをしましょうよってのは僕も思って提案してたから、良い傾向だと思います。

単にコードレビューといっても、やはり何らかの目的と視点を持ってみていかなければ脆弱性やバグは発見できないことがあります。レビューやテストに実は一番大切なのは「想像力」ではないかと思う今日この頃です。

簡単な例題でコードレビューの視点を考える

例題として「ウェブページに対する操作を行う以下のメソッドのコードレビューでは何を想像すれば良いか」というお題を考えてみましょう。

リクエストの形:
__mode=some_action&blog_id=1&id=100

問題のコード

sub some_action{
    my $app= shift;
    require MT::Page;
    my $page = MT::Page->load( $app->param( 'id' ) );
    if (! $page ) {
        return $app->translate( 'Invalid Page ID:[_1].', $app->param( 'id' ) );
    }
    my $perm = $app->user->is_superuser;
    if (! $perm ) {
        $perm = $app->user->permissions( $app->blog->id )->can_administer_website;
        $perm = $app->user->permissions( $app->blog->id )->can_administer_blog unless $perm;
        $perm = $app->user->permissions( $app->blog->id )->can_manage_pages unless $perm;
    }
    if (! $perm ) {
        return $app->translate( 'Permission denied.' );
    }
    # この後、$pageへの処理
}

例外として想像すべきケースは例えば以下のようなものです。

  1. パラメタidに数字以外のものが渡された場合
  2. パラメタidにpageオブジェクトではなくentryオブジェクトのidが渡された場合
  3. パラメタidが空の場合
  4. パラメタidに現在のblogに属さないentry/pageのidが渡された場合

1.のケースですが、数字以外のものが渡された場合、pageはロードされませんから、 return $app->translate( 'Invalid Page ID:[_1].', $app->param( 'id' ) ); が呼ばれます。return している際にパラメタ id を付けてメッセージを返していますが、idにJavaScriptが渡されたらどうなるでしょうか。このコードにはXSS脆弱性があります。

2.のケースでは、$pageに entryオブジェクトがそのまま格納されます(IDのみを指定した場合、classに関わらずロードされます)。ところが、この後の権限チェックではcan_manage_pagesで権限をチェックしています。このコードには権限のないオブジェクトに対する操作が可能な脆弱性があります。

3.のケースですが、my $page = MT::Page->load();となりますが、$pageには条件指定なしで呼び出されたオブジェクトの先頭1件が格納されてしまいます。操作の内容によりますが、これはバグである可能性があります。

4.のケースですが、entry_idが100のページがblog_idが2のブログに属していた場合どうなるでしょうか。 blog_idが1のブログに対してウェブページの管理権限を持っているかどうかのチェックを行っていますが、blog_idが2のブログの権限チェックはしていません。blog_idが2のブログにこのユーザーが権限を持っていない場合、このコードはそのまま実行されてしまいます。このコードには権限のないオブジェクトに対する操作が可能な脆弱性があります。

その他にも、オブジェクトに対する操作を伴うメソッドには magic_token によるチェック、及びcms_save_permission_filterやcms_delete_permission_filter等操作に応じたコールバックをコールして権限チェックを行わなければなりません。

例題のコードの修正すべき点は以下の通りです。

  1. idがなければその時点でエラーを返す(エラーメッセージに受け取ったパラメタを含める場合はencode_html)を忘れない(もしくはテンプレート側でエスケープする)。
  2. idが数字かどうかをチェックする(数字でなければエラー)、またはエラーメッセージにユーザーが指定可能なパラメタを含める場合、MT::Util::encode_htmlを通して返す(ettor.tmplの方でエラー出力のタグにescape="html"が含まれている場合は$app->errorメソッドを使うことでも良いですが、現状のMTではコード側でエスケープした方が安全)。
  3. 読み込んだ$pageの$page->classをチェックして、page以外であればエラーを返す。もしくはロードメソッドでclass=>'page'を指定して、オブジェクトがpageであることを保証する。
  4. $app->user->permissions()に渡すBlogIDを $app->blog->id ではなく、 $page->blog_id とする(もしくは$app->blog->id と $page->blog_id が異なる場合にエラーを返す*システムスコープで実行される可能性があるケースでは前者)。
  5. この後オブジェクトに何らかの操作を行うメソッドの場合は、magic_tokenのチェックを事前に入れる
  6. 保存、削除を伴うメソッドの場合、cms_save_permission_filter.pageコールバックをコールして、戻り値がない場合にエラーを返す(必要に応じてcms_save_filter.page/cms_pre_save.page/cms_post_save.pageをコールする)

修正適用後のコード(例)

sub some_action {
    my $app = shift;
    $app->validate_magic or
        return $app->trans_error( 'Permission denied.' );
    require MT::Page;
    my $page;
    if (! $app->param( 'id' ) ||
        (! $page = MT::Page->load( $app->param( 'id' ) ) ) {
        return $app->trans_error( 'Invalid Page ID:\'[_1]\'', MT::Util::encode_html( $app->param( 'id' ) ) );
    }
    if ( $page->class ne 'page' ) {
        return $app->trans_error( 'Invalid Class:\'[_1]\'', $page->class );
    }

    # permission check case 1

    if ( $app->blog->id != $page->blog_id ) {
        return $app->trans_error( 'Invalid BlogID:\'[_1]\'', MT::Util::encode_html( $app->blog->id ) );
    }    
    if (! $app->can_do( 'manage_pages' ) ) {
        return $app->trans_error( 'Permission denied.' );
    }
    
    # or case 2
    
    my $perm = $app->user->is_superuser; 
    if (! $perm ) {
        $perm = $app->user->permissions( $page->blog_id )->can_administer_website;
        $perm = $app->user->permissions( $page->blog_id )->can_administer_blog unless $perm;
        $perm = $app->user->permissions( $page->blog_id )->can_manage_pages unless $perm;
    }
    if (! $perm ) {
        return $app->trans_error( 'Permission denied.' );
    }

    if (! $app->run_callbacks( 'cms_save_permission_filter.page' $app, $page ) {
        return $app->trans_error( 'Permission denied.' );
    }
    # Do Something.
}

こんなことが本当にあるの? ってか、でも結構あるんですよね。だいぶ潰れてるとは思いますが、セキュリティ強化月間で徹底的にやっていただきたいものです。もちろん、弊社もセキュリティ強化月間です。

さて、実はもう一つ紹介したい例題があるのですが、さすがにちょっとここには書けないです。世の中には危ないコードがいっぱいありますからね。またいずれ気が変わったら書くかもしれません。

文字実体参照に変換したりURLエンコードしたりquotemetaしたり何かと面倒なので久しぶりにAppleScriptで書いてみた。

TextWranglerのスクリプトメニュー

とりあえずは下記の5種類。

  • 文字実体参照変換
  • 文字実体参照から戻す
  • URLエンコード
  • URLデコード
  • quotemeta(Perl)

↓TextWranglerユーザーの方はよろしければどうぞ。

# 直接PerlやPythonとかでも書けるみたい(今回は選択範囲とかの受け渡し方がちょっとわからんかったのでAppleScriptで書いた)。

~/Library/Application Support/TextWrangler/AppleScriptディレクトリを作成。

最初に定番?の文字列置換モジュールを書いてAppleScriptディレクトリに設置。

(*
TextReplacer.scpt
*)
on replace_all(theText, serchStr, replaceStr)
	set theDelim to AppleScript's text item delimiters
	set AppleScript's text item delimiters to serchStr
	set theList to every text item of theText
	set AppleScript's text item delimiters to replaceStr
	set theText to theList as string
	set AppleScript's text item delimiters to theDelim
	return theText
end replace_all

メニュースクリプトは ~/Library/Application Support/TextWrangler/Scripts 以下に置く

簡単な文字実体参照変換とか...

(*
To Character Entity Reference.scpt
*)
set libpath to path to library folder from user domain
set libpath to (libpath as Unicode text) & "Application Support:TextWrangler:AppleScript:TextReplacer.scpt"
set TextReplacer to load script file libpath

tell application "TextWrangler"
	set targetWin to window 1
	set selectionText to selection of targetWin as Unicode text
	set selectionText to TextReplacer's replace_all(selectionText, "&", "&amp;")
	set selectionText to TextReplacer's replace_all(selectionText, "<", "&lt;")
	set selectionText to TextReplacer's replace_all(selectionText, ">", "&gt;")
	set selectionText to TextReplacer's replace_all(selectionText, "\"", "&quot;")
	set selection of targetWin to selectionText
end tell

Perlのワンライナーを実行させたり...

(*
Perl Quotemeta.scpt
*)
tell application "TextWrangler"
	set targetWin to window 1
	set selectionText to selection of targetWin as Unicode text
	set selectionText to quoted form of selectionText
	set command to "perl -e"
	set perlScript to quoted form of "print quotemeta($ARGV[0])"
	set selectionText to do shell script command & " " & perlScript & " " & selectionText
	set selection of targetWin to selectionText
end tell

正規表現も...

(*
Encode URL.scpt
*)
tell application "TextWrangler"
	set targetWin to window 1
	set selectionText to selection of targetWin as Unicode text
	set selectionText to quoted form of selectionText
	set command to "perl -e"
	set perlScript to quoted form of "$s=$ARGV[0];$s=~s!([^a-zA-Z0-9_.~-])!uc sprintf \"%%%02x\", ord($1)!eg;print $s"
	set selectionText to do shell script command & " " & perlScript & " " & selectionText
	set selection of targetWin to selectionText
end tell

こんな配布の仕方もできるのね...

MT meets PHP.

| コメント(0) | トラックバック(0)

年末だというのに...

大晦日に何か面白いもの作って公開するってのを数年前までやっていたんだけど今年はちょっと無理ですね。年明け早々にはお披露目出来るかと思います(何が!?)。

標題は納会の時の僕の社内プレゼンのテーマですが、今PHPによるMTの拡張っていうテーマでコードをあれこれ書いてます。

さて、↓反応遅れましたが。

「育てる時代にふさわしいCMSベスト5」で一位に選んでいただきました(ありがとうございます...しかしあの電話が取材だったのか!?)。

といってもその1位がどうの、とか書かれている内容とは違うところに少し反応してみます。それは取り上げられているCMSの中で、

MT(Power CMS for MT)以外全て(多分)PHPで書かれている!んですよ。

とはいえMTもPower CMS for MTもPHPにも対応していて、ただ管理画面はPerl、出力形式をPerlによるスタティック生成とPHPによるダイナミックパブリッシングの両方をサポートしているということになります。

MTのダイナミックパブリッシングがPerlでなくPHPであることからもわかるように、やっぱりPerl CGIとかと比較して手軽に動的生成行うのならPHP選択ってことになると思います。FastCGIなんか入れるとキビキビ動くようにはなりますけど、とはいってもサーバーのメモリとか考えるとPHPの手軽さってやっぱりあるんだと思います。

ということは関係あるのかどうかわかんないんですが(関係ないです!)、この2ヶ月ほどMTのPHPのライブラリを見直してみつつ、MTのPHPフレームワークを拡張する新しいクラスを作っていてついでにちょっとした管理画面とかも作ってみました。

管理画面1

管理画面2

ちゃんと動くし、まぁ軽量っちゃー軽量ですね。アーカイブをダイナミックパブリッシングにしておけば本当にPHPのCMSです(本当って何だ?)。まぁ大規模サイトに導入するときはスタティック最強だと僕は思っていて、MTが選ばれるのもあの何ていうか(好き嫌いの多い)「再構築」があるからだと思うわけですが、PHPで書きたくなる気持ちも分かります。

で、PHPで拡張する仕組みを作って表示側をリアルタイムに変えられるようになると何だか書きたくなるもののパターンって変わるんですね。つまり何かWordpressっぽいというか、そういうのを書きたくなる。例えば携帯キャリアやスマートフィン対応とか検索ワードのハイライトとかLPOとか日本語URLとか、何か言語と言語をとりまく環境が作りたいものを左右するって不思議な感じです。

ところで、MTのダイナミックパブリッシングに「多くのプラグインが対応していないからMTのダイナミックパブリッシングは使えない」みたいな可哀想な言われ方をしていて(Power CMS for MTはすべてダイナミックパブリッシング対応ですが...400以上のPHPプラグインが!)、何で両対応しないかって、書き方のお作法が違うんですよね。MT5になってadodbをベースにしたclass.baseobject.phpってのが出来てFind()やLoad()やSave()も実装されてはいるものの、書き方のお作法が違うから両方対応するのが面倒になるわけです。なので、

    $categories = $app->load( 'Category', array( 'label' => $category ) );

とか

    $terms = array( 'blog_id' => $blog_id, 'id' => $id );
    $args  = array( 'limit' => 1 );
    $entry = $app->load( 'Entry', $terms, $args );

とか、

    $entry = $app->get_by_key( 'Entry', array( 'blog_id' => $blog_id, 'foo' => $foo ) );

とか

    $app->can_edit_entry($entry);

とか

    $foo = app->param('foo');

とか

    $user = app->user();

とか出来るようにしてみたら書きやすいんじゃないかと思ってそうしてみたら...

本当に書きやすいです。はやくやっとけばよかった。

PHPであれこれやってみて感じること

やっぱり軽快さというかそのあたりは使いたくなるのはわかるけど、変に直接HTMLの中に書ける手軽さは「ついつい」なコードを書いてしまいがち。なのでやっぱりMTならMTの作法でテンプレートエンジンによってアプリケーションのロジックとコンテンツをちゃんと分けるようにすべきだなぁと。

あと、関数名重複や変数のスコープの意識とか考えさせられますね。かといって function powercms_util_func_hoge() とか長ったらしい書き方を強要させられるのも何だかと思うので、ちゃんとクラス作成してとか名前空間指定してとか(って名前空間指定できるようになったのってPHP 5.3.0からなんだ!)って、PHP製のCMS作ってる人はど苦労してないんだろうかとか思いつつ、ちょっと本格的にPHPでMT製のサイトを拡張できる仕組みを作ってます。

管理画面のサンプルも近々公開できると思います。来年は案件用の管理画面とかについてもPerl/PHPの両方で提案できるようにしたいと思います。

と、いうことで(どういうことだ?)、皆様良いお年を。

11月25日、新しくなったPower CMS for MTのお披露目(セミナー)をします。新機能の話はまた追ってご紹介しますが、このエントリーでは自分用のメモも兼ねてパフォーマンス向上をどう図ったかについて書きなぐってみます。

計測する

MT.pmの_init_plugins_coreにどれくらいかかっているかをTime::HiResを用いて計測とかMySQLのクエリをmy.cnfに log=/path/to/logs/mysql.log とか設定して確認とか計測プラグインとかベンチマークテスト(Apach benchとか自前のスクリプトとかで計測。今回は50リクエストを同時に送る前提でテストしました)。いくつかの数値データはセミナー時にご紹介したいと思います(現状で最大で250%近く速くなってます)。

PerformanceLogging関連の設定をmt-config.cgiに記述することでログで計測も出来ます。

PerformanceLogging 1
PerformanceLoggingPath /var/log/mt/
ProcessLoggingThreshold 0.5

ファイル数を減らす、ファイルを軽量化する

まずは数を減らすこと、容量を減らすことで軽量化を図りました。プラグイン50とか100設置すると_init_plugins_coreにしても0.5秒近くかかったりします(ログを吐いて計測)。

追記。プラグインを無効化するときは管理画面で「無効」ではなくプラグインファイル自体を退避するか削除した方が速度的には有利になります。pluginsディレクトリ以下のconfid.yaml、またはplファイルは全部一度読み込まれてからPluginSwitchの設定で有効無効を判断されるからです。また、プラグインの読み込み順は環境に依存しますが、addonsが先に読み込まれます。読み込み順が動作に影響するものの場合は、先に認識されていないとまずいものについてはaddonsとして作り、それを利用するものをpluginとして作成すると良いと思います。

plugins/myplugin/直下の config.yaml もしくは myplugin.pl のファイル容量を減らす。initとかpre_run以外の処理、テンプレートタグや特定のコールバック処理等はモジュール化します。つまり

 'CMSContext' => \&_hdlr_cms_context,

は、以下のようにしてlib/PowerCMS/Tags.pmに持っていきます

 'CMSContext' => '$powercms::PowerCMS::Tags::_hdlr_cms_context',

プラグインを可能な範囲で統合します。統合することでファイル数が減ることはもちろん、プラグインの設定をロードする際のSQLのクエリを減らせます。統合の方針としては、相互依存があって着脱可能でないものや比較的単機能のもの。複雑なものについては慎重に行います。

プラグインデータはプラグイン毎に1レコードなので(正確にはsystem/blog毎に保存されている)プラグインが一つなら複数の値を持っていてもクエリはプラグイン毎に1回だけ呼ばれます。こんな感じのSQL。

SELECT *
FROM mt_plugindata
WHERE (plugindata_plugin = 'powercms')
AND (plugindata_key = 'configuration')
LIMIT 1

あるいは

SELECT *
FROM mt_plugindata
WHERE (plugindata_plugin = 'powercms')
AND (plugindata_key = 'configuration:blog:1')
LIMIT 1

プラグインが複数だと、こんなのが呼び出し時にプラグインの数だけ呼ばれます。統合すればクエリは1回になります。設定が移ることになるのでアップグレード時に移行。当然こういうのはアップグレード時だけ必要なコードなのでコードは本体には書かずにモジュールのほうに移します。

upgrade_functions => {
    config_settings => {
        plugin => 'PowerCMS',
        code => '$powercms::PowerCMS::Upgrade::_upgrade_functions',
    },
    config_settings_nv => {
        plugin => 'PowerCMS',
        version_limit => $SCHEMA_VERSION,
        code => '$powercms::PowerCMS::Upgrade::_upgrade_functions',
    } },
}

# 以下は外部モジュールに

sub _upgrade_functions {
    my $app = MT->instance();
    my $plugin_powercms = MT->component( 'PowerCMS' );
    require MT::PluginData;
    my $data = MT::PluginData->load( { plugin => 'old_plugin',
                                       key    => 'configuration' } );
    if ( $data ) {
        if ( my $cfg = $data->data() ) {
            my $old_plugin_setting = $cfg->{ old_plugin_setting_key };
            $plugin_powercms->set_config_value( 'new_plugin_setting', $old_plugin_setting );
        }
        $data->remove or die $data->errstr;
    }
    # ...
}

統合する際に、各プラグインで同じコールバックに対する処理を書いていたときは配列で指定します(config.yamlではなくfoo.plの場合)。

    'MT::App::CMS::template_param.preview_strip' => [
        { handler => '$powercms::OldPlugin1::Plugin::_preview_param', priority => 2, },
        { handler => '$powercms::OldPlugin2::Plugin::_preview_param', priority => 3, },
        { handler => '$powercms::OldPlugin3::Plugin::_preview_param', priority => 4,}
    ],

*もちろんコードを統合してもいいけれど。

* 統合とモジュール化する際の注意

プラグイン内にできるだけコードを書かずにモジュール化した関係で各モジュールの中で必要に応じてrequire Foo;するように。移植の際にMT::Fooのロードの時にそのあたりでハマらないためにできるだけ

MT::Entry->load( $terms );

ではなく、

MT->model( 'entry' )->load( $terms );

にする。プラグインのロード順は環境依存するので、モジュールが読み込めずに死んでしまう場合の対策にもなる。モジュールの各ルーチンの中でプラグインデータを取得する際には以下のように明示的に指定します。

my $plugin = MT->component( 'PowerCMS' );

システム全体の設定で変更するケースがほぼないものをmt-config.cgiへ

細かな話なのですが、プラグイン設定のデータに保存したものを呼び出すためにはSQLが発行されますので、システム全体で導入時に設定すれば変更しない一部のものをmt-config.cgiに記述する仕様に変更します。

クエリの実行結果をキャッシュする

同じmt.cgiへのリクエストに対する処理の中で同じSQLを再発行せずに使いまわします。

require MT::Request;
my $obj;
my $key = 'cache_key:' . $class . ':' . $obj_id;
$obj  = MT::Request->instance->cache( $key );
if (! $obj ) {
    $obj = MT-model( $class )->load( $obj_id );
    MT::Request->instance->cache( $key, $obj );
}
$obj;

MTが内部的にすでにキャッシュしているものはそっちを使うようにします。例えば、

my $blogs = MT::Blog->load( { parent_id => $website->id,
                              class => 'blog' } );

みたいなのは

my $blogs = $website->blogs;

を使う。MTの内部ですでにこれが呼び出されていたらキャッシュが使われるし、以後MTがこれを利用する際にはキャッシュが使われます。

追記。MT::OnjectのサブクラスをIDでロードする時はMT::Foo->load( { id => $id } ); ではなく MT::Foo->load( $id ); とします。そうすることで、->lookupが使われてSQLが複数発行されなくなります。そういう意味では上記のMT::Requestの例の効果は限定的です。

さらに追記。MT::Foo->cache_property( $key, sub{ [ some code ] } ); とすることで実行結果をキャッシュしつつキャッシュが無ければ実行結果を返すことが出来てこっちの方がコードはシンプルになります。以下、$website->blogs のコード。

# lib/MT/Website.pm
sub blogs {
    my $class = shift;
    my ($terms, $args) = @_;

    my $blog_class = MT->model('blog');
    if ($terms || $args) {
        $terms ||= {};
        $terms->{class} = 'blog';
        $terms->{parent_id} = $class->id;
        return [ $blog_class->load( $terms, $args ) ];
    } else {
        $class->cache_property('blogs', sub {
            [ $blog_class->load({
                parent_id => $class->id,
                class     => 'blog'
            }) ];
        });
    }
}

リクエストをまたがるキャッシュはMemcache、DBキャッシュ、ファイルキャッシュ等

require MT::Memcached;
if ( MT::Memcached->is_available ) {
    my $cache = MT::Memcached->instance;
    my $data = $cache->get( $key );
    return $data if $data;
    $data = 'foo';
    $cache->set( $key => $data );
    return $data;
} else {
    # ...
}

もしくはプラグインデータとか、MT::Sessionとかに一時的に保存してしかるべき時にクリアするとか。僕らは専用のテーブルを作っていてMTタグでキャッシュの定義が出来るようにしてあります。ついでに環境変数の設定によってDBの変わりにファイルキャッシュが使えるようにしています。MT::Cache::Negotiateを使うことでMemcachedが使えない時にMT::Sessionを使ってDBにキャッシュさせることが出来ます。こちらのコードはMemcachedが有効かどうかを気にする必要はありません。

require MT::Cache::Negotiate;
$cache_driver = MT::Cache::Negotiate->new( ttl => $ttl );
my $cache_value = $cache_driver->get( $cache_key );
$cache_value = utf8_on( $cache_value );
$cache_driver->replace( $cache_key,$cache_value, $ttl );

特定のウィジェットが表示されているかどうかで処理をスキップするなど

特定のダッシュボード用のCSSをhtml_head部で組み立てる時とか。ウィジェットが非表示なのに処理を行っているのは明らかに無駄なので判別するようにしました(判別するための処理に時間がかからない前提ならば)。以下の例はBlogTreeダッシュボードが無効な時にCSSを組み立てるための権限チェック等をスキップするための判定用のコードです。

sub __is_blogtree {
    my $app = shift;
    my $r = MT::Request->instance;
    my $is_blogtree = $r->cache( 'powercms_is_blogtree' );
    if ( $is_blogtree eq 'true' ) {
        return 1;
    } elsif ( $is_blogtree eq 'false' ) {
        return 0;
    }
    my $mode = $app->mode;
    my $view = $app->view;
    my $user = $app->user || return 0;
    my $bt;
    if ( $mode ne 'blogtree' ) {
        if ( ( $view eq 'user' ) || ( $mode eq 'default' ) || ( $mode eq 'dashboard' ) ) {
            my $widget_store = $user->widgets;
            if ( my $blog = $app->blog ) {
                my $blog_id = $blog->id;
                my $widgets = $widget_store->{ "dashboard:blog:$blog_id" } if $widget_store;
                $bt = $widgets->{ blog_tree } if $widgets;
            } else {
                my $user_id = $user->id;
                my $widgets = $widget_store->{ "dashboard:$view:$user_id" } if $widget_store;
                $bt = $widgets->{ blog_tree } if $widgets;
            }
        }
    } else {
        $bt = 1;
    }
    if ( $bt ) {
        $r->cache( 'powercms_is_blogtree', 'true' );
    } else {
        $r->cache( 'powercms_is_blogtree', 'false' );
    }
    return $bt;
}

ダイナミックパブリッシングのキャッシュの設定を適切にする

これは公開側の話。ウェブサイト/ブログの設定で「キャッシュする」、「条件付き取得を有効にする」を選択した上で、キャッシュさせたくないケースのみmtview.phpの中で分岐させる。例えばケータイ向けのページを除外するなど。

    $request_uri  = $_SERVER[ 'REQUEST_URI' ];
    if (! preg_match( "/\/m\//", $request_uri ) ) {
        $mt->conditional( true );
        $mt->caching( true );
    }
    $mt->view();

ダイナミックパブリッシングで条件付きGETに対応させる

MT標準の場合は既に対応しているので、DynamicMTMLの方を対応させました(スマートフォン、携帯等で分岐したりログインユーザー毎に違うものを出力するケースを除く)。

if ( $conditional && (! $author_id ) ) {
    $last_ts = $blog->blog_children_modified_on;
    if ( $if_modified && ( $if_modified >= filemtime( $existing_file ) ) ) {
        if ( ( strtotime( $last_ts ) <= $if_modified ) ) {
            header( 'HTTP/1.1 304 Not Modified' );
            exit();
        }
    }
    $orig_mtime = strtotime( $last_ts );
}
//...

管理画面のJavaScript/CSSを圧縮する

minifyを利用。プラグインで.htaccess (mod_rewriteの設定)を自動生成して導入を簡単にする仕組みを作成しました。バックエンドだけじゃなくCSS/JavaScriptの量も半端ないからです。

データベースの最適化、軽量化

./tools/optimize-mysql というスクリプトを作って定期的に最適化をcronジョブに登録できるようにしました。./tools/remove_old_sessions てのはMTに標準であるのであとはシステムログのローテーション。

処理に負担のかかるものを代替手段提供で選択できるように

アクセス解析がどうしてもDBの肥大化と処理のボトルネックになりがちだったので、処理はGoogleAnalytics に任せてMTと連携させ、レポートの表示やアクセスランキングのテンプレート出力が出来るようにしました。

FastCGIに対応させる

普通に拡張すればそのまま対応の筈なんですが、registryを動的に変更させたり等の処理をプラグインで行っている場合など、変更されたものが正しく初期化されない場合があります。ということで、take_downコールバックで設定関係を初期化することでおかしな挙動を回避可能になります。

sub _take_down {
    my ( $cb, $app ) = @_;
    return unless $ENV{FAST_CGI};
    my $cfg = $app->config; my $c = $app->find_config;
    $cfg->read_config( $c ); $cfg->read_config_db();
}

* こういうのやってると何か脳内にアドレナリンが出ます。いや、本当。

続き。

MTベースの開発案件の時に「DBのスキーマはどうなりますか? ウチの方でPHPでMySQL叩いて開発とか後でできるでしょうか?」みたいな話が出るので。静的に動的なページを吐き出す(??)サンプル書いてみました。

エントリーを20件リストで表示する、ただそれだけ。直に書いてしまうとせっかくのテンプレートとロジックの分離のメリットが活かされないのですが、部分的に動的な処理を入れたいといった時にはまぁこんなやりかたも出来るということを覚えておいても良いと思います。

インデックス・テンプレートを作成し、「公開→スタティック」「出力ファイル名→foo.php」として保存・再構築。ブログ記事のリストの部分は動的に都度処理されます。それだけですが...

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=<$MTPublishCharset$>" />
    <title>DPAPI Example - <MTBlogName escape="html"></title>
</head>
<body>
<?php
    include('<$MTCGIServerPath$>/php/mt.php');
    $mt = MT::get_instance(<MTBlogID>, '<$MTCGIServerPath$>/mt-config.cgi');
    require_once("MTUtil.php");
    $args['class'] = 'entry';
    $args['blog_id'] = <MTBlogID>;
    $args['offset']=0;
    $args['limit']=20;
    $entries = $mt->db()->fetch_entries($args);
    $max = count($entries);
    $i = 0;
    foreach ($entries as $entry) {
        if ($i==0) {
            echo '<ul>';
        }
        echo '<li>' . encode_html($entry->title) . '</li>';
        $i++;
        if ($i==$max) {
            echo '</ul>';
        }
    }
?>
</body>
</html>

スタッフの仕事の進捗がヤバげだったので急遽phpを担当することになってブロックタグの大物をMT5対応するためにガンダムBrand new APIと格闘してみたメモ書き。

# Power CMS for MTにはPHPプラグインが189もあるんだよ...

Twitter / Junnama Noda: MTのPHP API使ってアプリ開発してるのって日本でhoge人くらいなんだからこんなに力入れてガラッと変えなくても(泣)

さて、ブロックタグの中でのオブジェクトのロードや利用についてのメモを中心に。

エントリーをロード

$args['class'] = 'entry';
$args['blog_id'] = $blog_id;
$args['offset']=n;
$args['limit']=n;
$entries = $ctx->mt->db()->fetch_entries($args);

範囲指定をしてsqlでエントリー(や他のオブジェクト)を読み込む

$entries = $ctx->mt->db()->SelectLimit( $sql, $limit, $offset );

(範囲の指定無し)sqlを実行

$entries = $ctx->mt->db()->Execute( $sql );

読み込んだオブジェクト数の取得

$entries->RecordCount();

個々のレコードの取得

$entries->Move($counter);
$entry = $entries->FetchRow();

エントリーのオブジェクト化(但しcategoryやカスタムフィールドの読み込みはSQL側で調整する必要あり)

$e; 
while(list ($key, $val) = each($entry)) {
    if (preg_match('/^entry_/',$key)) {
        $e->$key = $val;
    }
}
# $ctx->stash('entry', $e);

特定のブログ記事オブジェクトの読み込み

$e = $ctx->mt->db()->fetch_entry($id);

特定のウェブページオブジェクトの読み込み

$e = $ctx->mt->db()->fetch_page($id);

Findでオブジェクトを読み込み

require_once 'class.mt_entry.php'; #entry
$_entry = new Entry;
$where = $where_statment;
$extra = array(
    'limit' => $limit,
    'offset' => $offset,
); 
$results = $_entry->Find($where, false, false, $extra);

続きがあります...

参考

続きです(当日のデモで関連付けがうまくいかなかったのが余程悔しかったのだろう...)。

この方法で出来ないのは、エントリーに対するタグの重み付けなんですね。例えば上記エントリーには「MTDDC」というタグが最も重みが付けられるべきで、そうなれば確実に当日も失敗? しなかったわけです。ところが現状の方法では単語の出現回数なんかでの重み付けがなされないのです。

これについてはMT::ObjectTagオブジェクトを拡張してタグの重み付けを付けるという方法なんかが考えられますが(出現回数なんかでスコアを付ける)、興味のある方はトライしてみても面白いんじゃないでしょうか。

と書いてみたものの...というか「興味のある方=俺」なので取り敢えずMT::ObjectTagに「score」というカラムを追加して出現回数を保存するようにしてみた。

scoreを追加したobjecttagテーブル

今回はここまででRelatedEntryを取り出すほうはまだなんだけどね。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 5.04

このアーカイブについて

このページには、過去に書かれたブログ記事のうちプログラミングカテゴリに属しているものが含まれています。

前のカテゴリはアクセシビリティです。

次のカテゴリはモブログです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。