Movable Type CMSプラットフォーム Movable Type
ドキュメントサイト

Manualマニュアル

第6回】グローバル・モディファイアプラグインの開発について

最終更新日: 2017.10.06

このページは既にhttps://github.com/movabletype/Documentation/wiki/Japanese-plugin-dev-1-6に移行されています。

はじめに

シックス・アパートの上野初仁(はつひと)です。

今回は具体的なプラグインとしてグローバル・モディファイアプラグインを作成していく手順を解説します。

グローバル・モディファイアとは?

グローバル・モディファイアは与えられた文字列に対して、すべて大文字に変換する、一部分にURLを付加するなどの加工処理を行います。

グローバル・モディファイアは、利用する際に'<MTEntryTitle lower_case="1">'といった形で、タグに対して付与し利用します。

プラグインの仕様とテストケースの作成

プラグインの仕様を決定し、それを元にテストケースの作成を作成しましょう。

プラグインの仕様

  1. タグによって与えられた文章をrot13暗号を用いて暗号化するグローバル・モディファイアを実装する。
  2. 呼び出し時の引数が1のときのみ動作する

テストケース(00-compile.t)

前回行ったMyPlugin03の内容と基本的に同じです。

use strict;
use lib qw( t/lib lib extlib );
use warnings;
use MT;
use Test::More tests => 5;
use MT::Test;

ok(MT->component ('MyPlugin04'), "MyPlugin04 plugin loaded correctry");

require_ok('MyPlugin04::L10N');
require_ok('MyPlugin04::L10N::ja');
require_ok('MyPlugin04::L10N::en_us');
require_ok('MyPlugin04::Tags');

1;

テストケース(01-tags.t)

前回行ったテストからJSON部分を修正し利用します。エントリーを1件取得し、そのタイトルに対してrot13暗号をかけます。

テストするのは以下の4項目です。
  1. 空文字列をインプットすると、空文字列が戻ってくる。
  2. エントリー1件を取得し、そのタイトル"A Rainy Day"が戻ってくる。
  3. エントリー1件を取得し、そのタイトル"A Rainy Day"をrot13暗号化しない"A Rainy Day"が戻ってくる。(rot13="0")
  4. エントリー1件を取得し、そのタイトル"A Rainy Day"をrot13暗号化した"N Enval Qnl"が戻ってくる。(rot13="1")
use strict;
use warnings;
use IPC::Open2;

BEGIN {
    $ENV{MT_CONFIG} = 'mysql-test.cfg';
}

$| = 1;

use lib 't/lib', 'lib', 'extlib';
use MT::Test qw(:db :data);
use Test::More;
use JSON -support_by_pp;
use MT;
use MT::Util qw(ts2epoch epoch2ts);
use MT::Template::Context;
use MT::Builder;

require POSIX;

my $mt = MT->new();

#===== Edit here
my $test_json = <<'JSON';
[
{ "r" : "1", "t" : "", "e" : ""},
{ "r" : "1", "t" : "<MTEntries lastn=\"1\"><MTEntryTitle></MTEntries>", "e" : "A Rainy Day"},
{ "r" : "1", "t" : "<MTEntries lastn=\"1\"><MTEntryTitle rot13=\"0\"></MTEntries>", "e" : "A Rainy Day"},
{ "r" : "1", "t" : "<MTEntries lastn=\"1\"><MTEntryTitle rot13=\"1\"></MTEntries>", "e" : "N Enval Qnl"}
]
JSON
#=====

$test_json =~ s/^ *#.*$//mg;
$test_json =~ s/# *\d+ *(?:TBD.*)? *$//mg;

my $json = new JSON;
$json->loose(1); # allows newlines inside strings
my $test_suite = $json->decode($test_json);

# Ok. We are now ready to test!
plan tests => (scalar(@$test_suite)) + 4;

my $blog_name_tmpl = MT::Template->load({name => "blog-name", blog_id => 1});
ok($blog_name_tmpl, "'blog-name' template found");

my $ctx = MT::Template::Context->new;
my $blog = MT::Blog->load(1);
ok($blog, "Test blog loaded");
$ctx->stash('blog', $blog);
$ctx->stash('blog_id', $blog->id);
$ctx->stash('builder', MT::Builder->new);

my $entry  = MT::Entry->load( 1 );
ok($entry, "Test entry loaded");

# entry we want to capture is dated: 19780131074500
my $tsdiff = time - ts2epoch($blog, '19780131074500');
my $daysdiff = int($tsdiff / (60 * 60 * 24));
my %const = (
    CFG_FILE => MT->instance->{cfg_file},
    VERSION_ID => MT->instance->version_id,
    CURRENT_WORKING_DIRECTORY => MT->instance->server_path,
    STATIC_CONSTANT => '1',
    DYNAMIC_CONSTANT => '',
    DAYS_CONSTANT1 => $daysdiff + 1,
    DAYS_CONSTANT2 => $daysdiff - 1,
    CURRENT_YEAR => POSIX::strftime("%Y", localtime),
    CURRENT_MONTH => POSIX::strftime("%m", localtime),
    TEST_JSON => "$test_json",
);

$test_json =~ s/\Q$_\E/$const{$_}/g for keys %const;
$test_suite = $json->decode($test_json);

$ctx->{current_timestamp} = '20040816135142';

my $num = 1;
foreach my $test_item (@$test_suite) {
    unless ($test_item->{r}) {
        pass("perl test skip " . $num++);
        next;
    }
    local $ctx->{__stash}{entry} = $entry if $test_item->{t} =~ m/<MTEntry/;
    $ctx->{__stash}{entry} = undef if $test_item->{t} =~ m/MTComments|MTPings/;
    $ctx->{__stash}{entries} = undef if $test_item->{t} =~ m/MTEntries|MTPages/;
    $ctx->stash('comment', undef);
    my $result = build($ctx, $test_item->{t});
    is($result, $test_item->{e}, "perl test " . $num++);
}

php_tests($test_suite);

sub build {
    my($ctx, $markup) = @_;
    my $b = $ctx->stash('builder');
    my $tokens = $b->compile($ctx, $markup);
    print('# -- error compiling: ' . $b->errstr), return undef
        unless defined $tokens;
    my $res = $b->build($ctx, $tokens);
    print '# -- error building: ' . ($b->errstr ? $b->errstr : '') . "\n"
        unless defined $res;
    return $res;
}

sub php_tests {
    my ($test_suite) = @_;
    my $test_script = <<'PHP';
<?php
include_once("php/mt.php");
include_once("php/lib/MTUtil.php");
require "t/lib/JSON.php";

$cfg_file = '<CFG_FILE>';
$test_json = '<TEST_JSON>';

$const = array(
    'CFG_FILE' => $cfg_file,
    'VERSION_ID' => VERSION_ID,
    'CURRENT_WORKING_DIRECTORY' => '',
    'STATIC_CONSTANT' => '',
    'DYNAMIC_CONSTANT' => '1',
    'DAYS_CONSTANT1' => '<DAYS_CONSTANT1>',
    'DAYS_CONSTANT2' => '<DAYS_CONSTANT2>',
    'CURRENT_YEAR' => strftime("%Y"),
    'CURRENT_MONTH' => strftime("%m"),
);

$output_results = 0;

$mt = MT::get_instance(1, $cfg_file);
$ctx =& $mt->context();

$path = $mt->config('mtdir');
if (substr($path, strlen($path) - 1, 1) == '/')
    $path = substr($path, 1, strlen($path)-1);
if (substr($path, strlen($path) - 2, 2) == '/t')
    $path = substr($path, 0, strlen($path) - 2);
$const['CURRENT_WORKING_DIRECTORY'] = $path;

$db = $mt->db();
$ctx->stash('blog_id', 1);
$blog = $db->fetch_blog(1);
$ctx->stash('blog', $blog);
$ctx->stash('current_timestamp', '20040816135142');
$mt->init_plugins();
$entry = $db->fetch_entry(1);

$suite = load_tests();

run($ctx, $suite);

function run(&$ctx, $suite) {
    $test_num = 0;
    global $entry;
    global $mt;
    global $tmpl;
    foreach ($suite as $test_item) {
        $mt->db()->savedqueries = array();
        if ( preg_match('/MT(Entry|Link)/', $test_item->t)
          && !preg_match('/MT(Comments|Pings)/', $test_item->t) )
        {
            $ctx->stash('entry', $entry);
        }
        else {
            $ctx->__stash['entry'] = null;
        }
        if ( preg_match('/MTEntries|MTPages/', $test_item->t) ) {
            $ctx->__stash['entries'] = null;
            $ctx->__stash['author'] = null;
            $ctx->__stash['category'] = null;
        }
        if ( preg_match('/MTCategoryArchiveLink/', $test_item->t) ) {
            $ctx->stash('current_archive_type', 'Category');
        } else {
            $ctx->stash('current_archive_type', '');
        }
        $test_num++;
        if ($test_item->r == 1) {
            $tmpl = $test_item->t;
            $result = build($ctx, $test_item->t);
            ok($result, $test_item->e, $test_num);
        } else {
            echo "ok - php test $test_num \n";
        }
    }
}

function load_tests() {
    $suite = cleanup('<TEST_JSON>');
    $json = new JSON();
    global $const;
    foreach ($const as $c => $r) {
        $suite = preg_replace('/' . $c . '/', $r, $suite);
    }
    $suite = $json->decode($suite);
    return $suite;
}
function cleanup($tmpl) {
    # Translating perl array/hash structures to PHP...
    # This is not a general solution... it's custom built for our input.
    $tmpl = preg_replace('/^ *#.*$/m', '', $tmpl);
    $tmpl = preg_replace('/# *\d+ *(?:TBD.*)? *$/m', '', $tmpl);
    return $tmpl;
}

function build(&$ctx, $tmpl) {
    if ($ctx->_compile_source('evaluated template', $tmpl, $_var_compiled)) {
        ob_start();
        $ctx->_eval('?>' . $_var_compiled);
        $_contents = ob_get_contents();
        ob_end_clean();
        return $_contents;
    } else {
        return $ctx->error("Error compiling template module '$module'");
    }
}

function ok($str, $that, $test_num) {
    global $mt;
    global $tmpl;
    $str = trim($str);
    $that = trim($that);
    if ($str === $that) {
        echo "ok - php test $test_num \n";
        return true;
    } else {
        echo "not ok - php test $test_num\n".
             "#     expected: $that\n".
             "#          got: $str\n";
        return false;
    }
}

?>
PHP

    $test_script =~ s/<\Q$_\E>/$const{$_}/g for keys %const;

    # now run the test suite through PHP!
    my $pid = open2(\*IN, \*OUT, "php");
    print OUT $test_script;
    close OUT;
    select IN;
    $| = 1;
    select STDOUT;

    my @lines;
    my $num = 1;

    my $test = sub {
        while (@lines) {
            my $result = shift @lines;
            if ($result =~ m/^ok/) {
                pass($result);
            } elsif ($result =~ m/^not ok/) {
                fail($result);
            } elsif ($result =~ m/^#/) {
                print STDERR $result . "\n";
            } else {
                print $result . "\n";
            }
        }
    };

    my $output = '';
    while (<IN>) {
        $output .= $_;
        if ($output =~ m/\n/) {
            my @new_lines = split /\n/, $output;
            $output = pop @new_lines;
            push @lines, @new_lines;
        }
        $test->() if @lines;
    }
    push @lines, $output if $output ne '';
    close IN;
    $test->() if @lines;
}

rot13グローバル・モディファイアの開発(Perl)

前章で作成したMyPlugin03を元にプラグインを作成します。

config.yaml

グローバル・モディファイアを追加には以下のように"tags"=> "modifier"=> "モディファイア名" => $プラグイン名::ハンドラ名 を記述します。
id: MyPlugin04
name: <__trans phrase="Sample Plugin rot13 globale modifier">
version: 1.0
description: <__trans phrase="_PLUGIN_DESCRIPTION">
author_name: <__trans phrase="_PLUGIN_AUTHOR">
author_link: http://www.example.com/about/
doc_link: http://www.example.com/docs/
l10n_class: MyPlugin04::L10N

tags:
    modifier:
        rot13: $MyPlugin04::MyPlugin04::Tags::_hdlr_rot13

L10N.pm

package MyPlugin04::L10N;
use strict;
use base 'MT::Plugin::L10N';

1;

L10N/en_us.pm

package MyPlugin04::L10N::en_us;

use strict;
use base 'MyPlugin04::L10N';
use vars qw( %Lexicon );

%Lexicon = (
    '_PLUGIN_DESCRIPTION' => 'Sample rot13 global modifier',
    '_PLUGIN_AUTHOR' => 'Plugin author',
);

1;

L10N/ja.pm

package MyPlugin04::L10N::ja;

use strict;
use base 'MyPlugin04::L10N::en_us';
use vars qw( %Lexicon );

%Lexicon = (
    'Sample Plugin rot13 globale modifier' => 'サンプルプラグイン rot13 グローバル・モディファイア',
    '_PLUGIN_DESCRIPTION' => 'rot13 テストプラグイン',
    '_PLUGIN_AUTHOR' => 'プラグイン作者',
);

1;

Tags.pm

rot13の実装です。$arg(引数)に1が設定されていない場合は、そのままreturnします。1が設定されていた場合はrot13暗号をかけた$strをreturnします。
package MyPlugin04::Tags;
use strict;

sub _hdlr_rot13 {
    my ($str, $arg, $ctx) = @_;
    return $str if $arg != 1;

    $str =~ tr/a-zA-Z/n-za-mN-ZA-M/;

    return $str;
}

1;

ファイルの配置

$MT_DIR/
|__ plugins/
   |__ MyPlugin04/
      |__ config.yaml
      |__ lib/
      |  |_ MyPlugin04/
      |     |__ L10N.pm
      |     |_ L10N/
      |     |  |_ en_us.pm
      |     |  |_ ja.pm
      |     |_ Tags.pm
      |__ t/
         |_00-compile.t
         |_01-tags.t

rot13グローバル・モディファイアの開発(PHP)

PHP版のプラグイン開発を行います。

modifier.rot13.php

ファイル名はグローバル・モディファイアである事と、モディファイア名がrot13である事から、modifier.rot13.phpとなります。
<?php
    function smarty_modifier_rot13($str, $args) {
        if ($args != 1) {
            return $str;
        }

        return str_rot13($str);
    }
?>

function名はsmarty記法にのっとってsmarty_modifier_rot13となります。

また実装に関してはPHP4.2以降にはrot13を行う"str_rot13()"関数があるので、そちらを用います。

ファイルの配置

$MT_DIR/
|__ plugins/
   |__ MyPlugin04/
      |__ config.yaml
      |__ lib/
      |  |_ MyPlugin04/
      |     |__ L10N.pm
      |     |_ L10N/
      |     |  |_ en_us.pm
      |     |  |_ ja.pm
      |     |_ Tags.pm
      |__ php/
      |  |_modifier.rot13.php
      |__ t/
         |_00-compile.t
         |_01-tags.t

テストの実行

では、作成したプラグインに対してテストを行います。

00-compile.t

$ perl plugins/MyPlugin04/t/00-compile.t 
1..5
ok 1 - MyPlugin04 plugin loaded correctry
ok 2 - require MyPlugin04::L10N;
ok 3 - require MyPlugin04::L10N::ja;
ok 4 - require MyPlugin04::L10N::en_us;
ok 5 - require MyPlugin04::Tags;

01-tags.t

$ perl plugins/MyPlugin04/t/01-tags.t
1..7
ok 1 - 'blog-name' template found
ok 2 - Test blog loaded
ok 3 - Test entry loaded

ok 4 - perl test 1
A Rainy Day
ok 5 - perl test 2
N Enval Qnl
ok 6 - perl test 3
ok 7 - ok - php test 1 ok - php test 2 ok - php test 3 

proveコマンドでのテスト

$ prove plugins/MyPlugin04/t/*.t
plugins/MyPlugin04/t/00-compile....ok                                        
plugins/MyPlugin04/t/01-tags.......ok                                        
All tests successful.
Files=2, Tests=12, 24 wallclock secs (12.23 cusr +  4.98 csys = 17.21 CPU)

全てのテストにパスし、初めに考えた仕様どおりに動作している事がわかります。

まとめ

グローバル・モディファイアは色々な場面で利用されます。(グローバル・モディファイアリファレンス

新規でファンクションタグを作成しなくても、新規にグローバル・モディファイアを作成する方が簡単な場合があります。これは状況や条件によるので、ケースバイケースでプラグインを作成してください。

プラグイン開発ステップ・バイ・ステップ シーズン1はこれで終了です。プラグイン作成の大枠を体感していただけたでしょうか?シーズン2ではさらにブロックタグやコンディショナルタグなどの解説に進む予定です。

プラグイン開発ステップ・バイ・ステップ インデックス

目次