読者です 読者をやめる 読者になる 読者になる

あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

Ruby の method_missing 的な tag_missing コールバックを MT に追加してみる

MovableType

Movable Type Advent Calendar 2013 の8日目です。

Rubymethod_missing だったり、PerlAUTOLOAD 的なものが、MT のタグを利用する時に使えると便利かなと思って、TagMissingプラグインなるものを作ってみました。存在しないタグを利用しようとした時に、tag_missing というコールバックを呼ぶ単純なものです。ダイナミックは未対応です。

ただ、これを実現する方法は、MT の処理をモンキーパッチするしかなさそうだったのと、作ってみたものの、インターフェイスも微妙な感じがしますが、せっかくなので公開します。

TagMissing プラグイン

タグが存在しない時のエラー処理は、MT::Builder::build を見る限り、下記のあたりにあって、

            my $hdlr = $ctx->handler_for( $t->tag );
            my ( $h, $type, $orig ) = $hdlr->values;
            my $conditional = defined $type && $type == 2;

            if ($h) {
# ... 省略 ...
            }
            else {
                if ( $t->tag !~ m/^_/ ) {    # placeholder tag. just ignore
                    return $build->error(
                        MT->translate( "Unknown tag found: [_1]", $t->tag ) );
                }
            }

https://github.com/movabletype/movabletype/blob/de8fc5aba805a113075aa7e58882855554dd5d5a/lib/MT/Builder.pm#L475-L591

MT::Template::Context::handler_for メソッドの結果次第で、分岐の経路が変わる感じなので、MT::Template::Context::handler_for の中にコールバックを仕込むのが確実かなと思ったので、TagMissing プラグインでは、handler_for をパッチする方針にしました。

モンキーパッチする時は、本体のバージョンアップの影響をなるべく受けないようにした方が良いので、無駄が多い感じですが、handler_forをコールバック前後で2回呼び出す形にしました。

package TagMissing::Patch;

use strict;
use warnings;

sub init {}

use MT::Template::Context;

my $orig_handler_for = \&MT::Template::Context::handler_for;

{
  no warnings 'redefine';
  *MT::Template::Context::handler_for = sub {
    my $ctx = shift;
    my $tag = lc $_[0];
  
    my $hdlr = $orig_handler_for->( $ctx, $tag );
    my ( $h ) = $hdlr->values;
  
    MT->run_callbacks( 'tag_missing', $ctx, $tag ) unless $h;

    $orig_handler_for->( $ctx, $tag )
  
  };
}

1;

https://github.com/taiju/mt-plugin-TagMissing/blob/8b69c4f3c6f372de7de032b68260c042a4b29eea/plugins/TagMissing/lib/TagMissing/Patch.pm

インターフェイス的にかなり微妙な感じですが、tag_missing コールバックの中で、$ctx->{__handlers}{tag_name_foo}に MT::Templage::Handler 形式でハンドラを代入するなどして、タグの動作を動的に指定することができます。

AutoUnless プラグイン

そんな感じで、tag_missing コールバックを使ったタグのリファレンス実装として、AutoUnless プラグインというのも作ってみました。

mt:IfWebsite みたいな、If という単語の含まれたタグがいろいろありますが、mt:UnlessWebsite みたいな逆の意味を持つタグがあったりなかったりするので、mt:*unless*がなければ自動的に定義するプラグインです。

tag_missing が呼ばれた時に、動的に定義する感じの実装で、tag_missing コールバックのサンプルプラグインにもなっています。

MT の t ディレクトリにある、35-tags.t を丸々移して、mt:*If*mt:*Unless* に書き換えた上で、テスト結果を逆にしたテストを書いてみましたが、一部を除いてうまく動いているようです。

https://github.com/taiju/mt-plugin-AutoUnless/blob/master/plugins/AutoUnless/t/tags.t

動的にいくつかのタグを定義するような時には、tag_missing コールバックは便利に使えそうです。

まとめ

あると便利な気はするんですけど、アイデアありきで作ってみたものの、実装は正直微妙な感じになってしまいました。

今年はコードを書くより、読む時間が圧倒的に多い年でした。来年は、書く時間を増やせたらいいなと思います。