モダンPerlの世界へようこそ

第41回HTML::Template::Pro:テンプレートに極力コードを書かせたくないときは

何でも埋め込めるのは楽ですが

前回紹介したHTML::MasonText::MicroTemplateのように生のPerlコードを埋め込めるテンプレートエンジンは、Perlをよく知っている人が画面の設計からウェブアプリケーションのコーディングまでひとり(ないし、よく統制のとれた少人数のチーム)で行うときには非常に手軽で便利なものです。

ただし、なんでも書けるからといって、たとえばテンプレートの中でO/Rマッパのメソッドを直接呼び出すコードを書いてしまうと、そのテンプレートは(利用するO/Rマッパの性質にもよりますが)おそらく実際に動作するデータベースやそれに付随するテストデータを用意しないと、途中で「Can't call method "..." on an undefined value」などのエラーが発生してレンダリングできなくなってしまいます。また、アプリケーションの設定にあわせてURLを生成するコードが埋まっていたり、コンテキストオブジェクト中のセッションオブジェクトの中のパラメータの値によって条件分岐するコードが埋まっている場合もアプリケーション本体やコンテキストオブジェクトが必要になるでしょうし、このようなオブジェクトは概して複雑なものになりがちですから、初期化に時間がかかったり、レンダリングには不要な大量のパラメータを渡したりしなければならなくなります。

そのこと自体は、モデル・ビュー・コントローラのような大きな枠組みの中ではある程度仕方のないことですが、このままでは、たとえばクロス・サイト・スクリプティング(XSS)対策が正しくできているか確認するテストを書きたくなっても、テンプレート単体でのテストはできず、テストのたびにデータベースなりアプリケーションなりを用意する作業が必要になってしまいます。これではテストも書きづらいですし、テンプレートを書き換えるたびにテストを走らせようという気にもなれません[1]⁠。

テンプレートからコードを追い出す

この問題を解決する方法はいくつか考えられますが、おそらくもっとも根本的な解決策は、テンプレートや、テンプレートをレンダリングするビューのクラスを整理して、外部依存しがちなコードをできるだけ外に出してしまうことでしょう。

いわゆるモデル層については、連載第8回でもReactionの例を紹介したように、データベースなどの外部ツールをラップする層と、O/Rマッパより上位のビジネスロジックを格納する層に分割するやり方などが知られるようになってきましたが、ビュー層も同じように、テンプレートに(オブジェクトではない)静的なデータを埋め込むエンジン部分と、各種のオブジェクトからテンプレートに埋め込むデータを取り出すロジック部分を分けてやれば、少なくともXSS対策などのテストは簡単になります。

もう少し具体的にいうと、たとえばO/Rマッパのイテレータなどは、そのままテンプレートに埋め込むのではなく、テンプレートの外であらかじめイテレータを回して必要なデータを配列やハッシュに格納してから、その配列やハッシュをテンプレートに渡すようにすればデータベースへの依存を取り除けるでしょうし、テンプレートに複雑な条件分岐がある場合は、その条件もあらかじめ簡単な変数にまとめておくと、わざわざレンダリングしなくてもその条件分岐の妥当性をテストできます。

このような処理は、メモリの使用量や処理時間の点ではかならずしも効率がよいとは限りませんし、アプリケーションやフレームワークの仕様が複雑になりすぎると結局どこにコードを記述すればよいかわからなくなって、見た目にもわかりやすいテンプレートにコードを埋め込んでお茶を濁すようなことにもなりがちですから、何が何でも関心の分離を追求すればよいというものでもないのですが、そのような制約を承知のうえで安全面を優先するなら、作業する人次第でどうにでもなってしまう約束事に頼るよりは、最初からテンプレートの中では任意のメソッドを呼び出せないテンプレートエンジンを使うほうが確実です。そのようなコードとデザインの分離に重点をおいたエンジンの代表例が、今回取り上げるHTML::Templateとその仲間です。

HTML::Template

ドットコムバブル真っ盛りの1999年6月に生まれたHTML::Templateは、既存のHTML::MasonやHTML::Embperlを使ったアプリケーションではないがしろにされがちだったコードとテンプレートの分離を強制するために、簡単な条件文やループのほかは、変数の値を埋め込むことしかできないようになっているのが最大の特徴といえます。

一例として、CPANの更新情報データベースから最新10件のデータを取り出して一覧にするHTMLページのテンプレートを考えてみましょう。ここでは連載第39回で紹介したORDB::CPANUploadsを利用してデータを取得することにします。第39回の記事ではORDB::CPANUploads::Uploadsのiterateメソッドを使って一行ごとに処理していきましたが、今回はselectメソッドを使って該当行のオブジェクトを一気に取り出してみました。生のPerlを扱えるエンジンであればselectメソッドの結果をそのままテンプレートに渡せばよいのですが、HTML::Templateの場合、テンプレートの中ではオブジェクトのメソッドを呼び出せないので、あらかじめ配列の中のオブジェクトの値を取り出しておく必要があるのがポイントです[2]⁠。

use strict;
use warnings;
use HTML::Template;
use ORDB::CPANUploads;
use Time::Piece;

my $users = ORDB::CPANUploads::Uploads->select("order by released desc limit 10");

my $template = <<'TMPL';
<html>
<body>
<TMPL_IF NAME="users">
<ul>
<TMPL_LOOP NAME="users">
<li><TMPL_VAR NAME="dist"> (<TMPL_VAR NAME="author">; <TMPL_VAR NAME="released">)</li>
</TMPL_LOOP>
</ul>
<TMPL_ELSE>
<p>Not found</p>
</TMPL_IF>
</body>
</html>
TMPL

my %params = (users => [
  map { +{
    dist     => $_->dist,
    author   => $_->author,
    released => Time::Piece->new($_->released)->ymd,
  }} @$users
]);

my $ht = HTML::Template->new(
  scalarref      => \$template,
  default_escape => 'HTML',
);

$ht->param(%params);
$ht->output(print_to => \*STDOUT);

なお、HTML::Templateでは埋め込む値をparamメソッドで渡すのが原則ですが、CGI.pmApache::RequestなどCGI.pm互換のparamメソッドを持つオブジェクトの値をそのまま埋め込みたい場合は、associateオプションを利用することでparamの値をコピーする作業を省略できます。


use strict;
use warnings;
use HTML::Template;
use CGI;

my $cgi = CGI->new;
my $ht = HTML::Template->new(
  associate      => $cgi,
  default_escape => 'HTML',
);

# $ht->param($_ => $cgi->param($_)) for $cgi->param; は不要

HTML::Template::Expr

HTML::Templateは、HTMLに似たタグを使っているため既存のHTML作成ツールを使うデザイナでも対応しやすいとか、ファイルをひとつコピーすればレンタルサーバでも簡単に使えるという手軽さもあって、ひと頃はずいぶん人気を集めたものですが、生のPerlを使えるテンプレートに慣れた人にはいささか面倒に思える部分もありました。

たとえば、HTML::Templateを使ってテーブルを組むときに、未定義値(undef)が入っているセルだけは特殊な扱いをしたい、という要件があったとします。このような場合、HTML::TemplateのTMPL_IFタグでは真偽のチェックしかできず、0などが入ったときに誤動作するので、あらかじめ以下のようにすべてのデータに対して値が定義されているかどうかのフラグを用意しなければなりません。

use strict;
use warnings;
use HTML::Template;

my $template = <<'TMPL';
<html><body>
<table>
<TMPL_LOOP NAME="rows">
<tr>
<th><TMPL_VAR NAME="key"></th>
<td>
  <TMPL_IF NAME="is_defined">
    <TMPL_VAR NAME="value">
  <TMPL_ELSE>
    undefined
  </TMPL_IF>
</td>
</tr>
</TMPL_LOOP>
</table>
</body></html>
TMPL

# この値は実際にはデータベースなどから取得するものです
my $rows = [
  { key => 'key1', value => 'value1' },
  { key => 'key2', value => undef    },
  { key => 'key3', value => 0        },
];

# ここでテンプレート内で使うフラグのデータを追加しています
for (@$rows) {
  $_->{is_defined} = defined $_->{value} ? 1 : 0;
}

my $ht = HTML::Template->new(
  scalarref      => \$template,
  default_escape => 'HTML',
);
$ht->param(rows => $rows);
$ht->output(print_to => \*STDOUT);

ただし、このように一度しか使わないフラグのためにいちいち名前をつけて対応するデータを用意するのは、⁠特に用意するフラグの数が多くなってくると)手間もかかりますし、メモリなどにもやさしくありません。

そのため、2001年にはテンプレート内でいくつかの基本的な関数を利用できるようにするHTML::Template::Exprがリリースされました。これを使うと、先ほどの例はこのように書き換えられます[3]⁠。

use strict;
use warnings;
use HTML::Template::Expr;

my $template = <<'TMPL';
<html><body>
<table>
<TMPL_LOOP NAME="rows">
<tr>
<th><TMPL_VAR NAME="key"></th>
<td>
  <TMPL_IF EXPR="defined(value)">
    <TMPL_VAR NAME="value">
  <TMPL_ELSE>
    undefined
  </TMPL_IF>
</td>
</tr>
</TMPL_LOOP>
</table>
</body></html>
TMPL

my $rows = [
  { key => 'key1', value => 'value1' },
  { key => 'key2', value => undef    },
  { key => 'key3', value => 0        },
];

my $ht = HTML::Template->new(
  scalarref      => \$template,
  default_escape => 'HTML',
);
$ht->param(rows => $rows);
$ht->output(print_to => \*STDOUT);

必要があれば任意の関数を登録することもできます。

use strict;
use warnings;
use HTML::Template::Expr;

my $template = <<'TMPL';
<TMPL_VAR EXPR="url('/foo')">
TMPL

my $ht = HTML::Template::Expr->new(
  scalarref      => \$template,
  default_escape => 'HTML',
  functions => {
    url => sub { my $path = shift; "http://example.com$path" },
  },
);

$ht->output(print_to => \*STDOUT);

HTML::Template::Pro

HTML::TemplateやHTML::Template::Exprは、CGIアプリケーションのような非永続環境で比較的小さなテンプレートを処理する分には十分高速ですし、速度を稼ぐためにさまざまなキャッシュ機構も用意されていますが、それでも永続環境で処理回数が増えたりテンプレートが大きく複雑になってくると、XS/Cを使っているライバルより遅くなったりメモリの使用量が大きくなりすぎるという問題がありました。

この問題に対処するため、2001年11月にはHTML::Template::JITというテンプレートをInline::Cでバイナリにコンパイルしてしまう実験的なモジュールも作られたのですが、より本格的な対策としては、2005年に登場したHTML::Template::Proを使うのが定番となっています。

このモジュールはHTML::Template(::Expr)とほぼ互換性を持つようにXS/Cで書き起こされたもので、Perl Template Roundupというプロジェクトのベンチマークによると、非永続環境ではいまでも群を抜いて高速ですし、永続環境でも、最近でこそさらに高速なエンジンが出てきたとはいえ、Template Toolkitなどよりは速いという結果が出ています。

また、乗り換えを簡単にするために、use HTML::Templateの部分をuse HTML::Template::Proに書き換えるだけで高速化の恩恵を受けられる(それ以外の、オブジェクトを生成する部分などは差し替える必要がない)ようになっているのもありがたいところ。HTML::Template::ProのFAQにも開発時はデバッグオプションが充実している素のHTML::Templateを使い、安定したらuse文をHTML::Template::Proに差し替えるという手法が紹介されていますが、いまでもHTML::Templateを使っているプロジェクトがあるなら、少なくとも数倍の高速化は期待できるのでHTML::Template::Proへの移行を検討したほうがよいでしょう(特殊なことをしていなければ宣言部を1行書き換えるだけで済むはずです⁠⁠。

HTML::Template::Compiled

HTML::Template::Proと同時期に生まれたものとしては、もうひとつ、HTML::Template::Compiledというモジュールもあります。こちらはHTML::TemplateのテンプレートをPerlのコードに変換するのが特徴で、XS/Cを利用しているHTML::Template::Proほどの速度は出ないものの、メモリキャッシュを目一杯効かせれば素のHTML::Templateはもとより永続環境のTemplate Toolkitよりも速くなるのですが、HTML::Template::Compiledは、生のPerlコードを埋め込めるようにするTMPL_PERLというタグが追加されていたり、テンプレートの中からメソッドを呼び出せるようになっていたりと、HTML::Templateの世界ではかなりの異端児です。HTML::Template(::Pro)にTemplate Toolkit風のドット構文を追加するフィルタ/プラグインはほかにもいくつか存在していますが、これらは、昔はともかく、圧倒的に高速なTTの代替品が存在しているいまとなっては、テンプレートの外部依存性が低いというHTML::Templateの長所を危うくするだけのものかもしれません。

HTML::Template::Bundle

HTML::Templateの仲間は、機能が意図的に限定されているのが特徴であり、すでに十分な実績もあるため新規の開発はほとんど止まっていたのですが、最近になって、これまでメーリングリストなどで話題になったり、兄弟分のモジュールで実験的に導入されたりした機能やノウハウをまとめて、コア機能を少し拡充しようという動きが出てきました(メーリングリストでは<TMPL_ELSIF>というタグの追加や、ユーザ定義のエスケープの導入、レキシカル変数の扱いの改善などが話題になっていました⁠⁠。その成果はまだCPANにはあがっていませんが、その努力の一環として、HTML::Template::Bundleというディストリビューションが公開されています。原稿執筆時点では本来リリースする権限のないモジュールが含まれているUNAUTHORIZED RELEASEなのであまり取り上げるべきものでもないのですが、ここには改変版のHTML::Templateが含まれているので、HTML::Templateの仲間を使っている人なら内容を確認しておいたほうがよいかもしれません。

おすすめ記事

記事・ニュース一覧