HTML::Template の使い方

HTML::Template は Perl の代表的なテンプレートエンジンのモジュールの一つです。 テンプレートの構文が HTML タグのようなところが大きな特徴です。 HTML::Template は、近年のテンプレートエンジンがサポートするような細かい機能はありませんが、 一般的なウェブアプリケーション開発で必要とされる最小限の機能が用意されており、 軽量かつ学習コストが低いテンプレートエンジンと言えるでしょう。 近年は大きなアップデートもなく成熟したテンプレートエンジンです。

目次

概要

まずは、HTML::Template の使い方を一通り見てみましょう。 次の内容のテンプレートファイル template.html を用意します。

<h1>会員一覧 (<TMPL_VAR NAME=year>年<TMPL_VAR NAME=month>月<TMPL_VAR NAME=day>日現在)</h1>
<TMPL_IF NAME="msize">
    <ul>
        <TMPL_LOOP NAME="mlist">
            <li><TMPL_VAR NAME=name ESCAPE=HTML> (<TMPL_VAR NAME=age>)</li>
        </TMPL_LOOP>
    </ul>
</TMPL_IF>

次に、テンプレートファイル template.html と同じディレクトリ位置に次の Perl スクリプトを用意します。 この Perl スクリプトは、同じディレクトリ位置にあるテンプレートファイル template.html を読み込み、 変数をセットしたうえで STDOUT に出力します。

use utf8;
use Encode;
use HTML::Template;

# 変数定義
my @now   = localtime;
my $mlist = [ { name => '太郎', age => 24 }, { name => '次郎', age => 21 } ];
my $msize = scalar @{$mlist};

# オブジェクト生成
my $template = HTML::Template->new(
    filename => 'template.html',
    utf8     => 1
);

# 変数をセット
$template->param( day   => $now[3] );
$template->param( month => $now[4] + 1 );
$template->param( year  => $now[5] + 1900 );
$template->param( mlist => $mlist );
$template->param( msize => $msize );

# 出力
my $output = $template->output();
print Encode::encode( 'UTF-8', $output );

この Perl スクリプトと同じディレクトリ位置をカレントディレクトリとします。 そして、この Perl スクリプトを実行すると、その出力結果は次の通りになります。

<h1>会員一覧 (2023年3月26日現在)</h1>

    <ul>

            <li>太郎 (24)</li>

            <li>次郎 (21)</li>

    </ul>

以上のサンプルだけでも、テンプレートの書き方とテンプレートの処理の方法をざっくりと理解できたのではないでしょうか。 以降、この HTML::Template の詳細を解説していきます。

オブジェクトの生成

HTML::Template を使うには、Perl スクリプトで HTML::Template のオブジェクトを生成しなければいけません。 このオブジェクトの生成時に与えるパラメータで HTML::Template の挙動が決まります。

my $template = HTML::Template->new(
    filename => 'template.html',
    utf8     => 1
);

これは冒頭のサンプルコードの抜粋ですが、パラメータは他にも数多く用意されています。 以下に良く使うであろうパラメータを紹介します。

scalarref => $ref_to_template_text

テンプレートをファイルではなくスカラー変数のリファレンスで指定します。 具体的な使い方は、当サイトの記事「Perl のテンプレートエンジンいろいろ」 の "HTML::Template" の説明をご覧ください。

filename => 'template.html'

読み込みたいテンプレートファイルのファイル名を指定します。 デフォルトではカレントディレクトリから指定のファイル名を探します。

多くのシーンでは、テンプレートファイルがカレントディレクトリに保存されていることはないでしょう。 その場合は、後述する path パラメータを使って検索ディレクトリパスを定義します。

utf8 => 1

テンプレートファイルを UTF-8 として読み込み、内部文字列としてロードします。 use utf8 宣言した Perl スクリプトであれば、Encode モジュールの decode 関数を使って外部文字列を内部文字列に変換する手間が省けます。

die_on_bad_params => 0

デフォルトでは、テンプレートに指定がない変数を HTML::Template オブジェクトの param() メソッドに指定してしまうと例外が投げられます。 しかし、このパラメータに 0 をセットすると、テンプレートに記述がない変数をセットしても例外が投げられません。

vanguard_compatibility_mode => 1

このパラメータに 1 を指定すると、テンプレート変数 <TMPL_VAR NAME=varname> の記述を %varname% という記述でも受け入れるようにします。 ただし、この記法を使うとテンプレート記述で HTML エスケープなどの変換が利用できなくなりますので、 自身で事前に HTML エスケープしたうえで値をセットしなければいけません。

path => ['path_to_dir']

パラメータ filename で指定したテンプレートファイルの検索対象となるディレクトリパスのリストを指定します。 FindBin モジュールが利用できるのであれば、path => ["$FindBin::Bin"] と記述すれば、 スクリプトをどの位置から呼び出してもスクリプトと同じディレクトリを検索対象としてくれます。 実際の使い方は、後述のコードサンプルをご覧ください。

case_sensitive => 1

デフォルトではテンプレート変数の大文字・小文字は区別されません。 このパラメータに 1 を指定すると、大文字・小文字を区別して処理されます。 たとえば、変数名が varname の場合、テンプレートに <TMPL_VAR NAME=varname> と記述すれば期待通りに変換されますが、 <TMPL_VAR NAME=VARNAME> と記述すると変換されません。

なお、大文字・小文字の区別の対象は変数名のみです。 このパラメータに 1 をセットしたとしても、構文の大文字・小文字は区別されません。 つまり、<TMPL_VAR NAME=varname> でも <tmpl_var name=varname> でも 変換されます。

loop_context_vars => 1

このパラメータに 1 をセットすると、繰り返し処理のアドオン変数が利用できるようになります。 具体的には、ループの最後だけ処理する、奇数行だけ処理する、などの便利な機能が有効になります。 詳細は後述します。

global_vars => 1

このパラメータに 1 をセットすると、繰り返し処理の外で使われる変数をループ内でも使えるようにします。 詳細は後述します。

default_escape => 'html'

変数を変換する際のエスケープ処理を指定することができます。 指定可能な値は html, js, url, none (デフォルト) のいずれかです。

このパラメータに html を指定しておくと、テンプレート側では <TMPL_VAR NAME=name ESCAPE=HTML> の代わりに <TMPL_VAR NAME=name> とすれば良くなります。 もしパラメータ vanguard_compatibility_mode が指定された場合は、 %varname% という記法でも HTML エスケープされるようになります。

以上が HTML::Template オブジェクト生成時に指定できるパラメータですが、私はよく次のように指定して使っています。

my $template = HTML::Template->new(
    filename                    => 'template.html',
    utf8                        => 1,
    vanguard_compatibility_mode => 1,
    path                        => ["$FindBin::Bin"],
    case_sensitive              => 1,
    loop_context_vars           => 1,
    global_vars                 => 1,
    default_escape              => 'html'
);

こうすると、冒頭のサンプルのテンプレートは次のように簡素化されます。 かなりシンプルになったと思いませんか?

<h1>会員一覧 (%year%年%month%月%day%日現在)</h1>
<TMPL_IF NAME="msize">
    <ul>
        <TMPL_LOOP NAME="mlist">
            <li>%name% (%age%)</li>
        </TMPL_LOOP>
    </ul>
</TMPL_IF>

変数

構文

テンプレート変数の構文は、標準では <TMPL_VAR NAME=varname> です。 そして、スクリプトで HTML::Template オブジェクトの param() メソッドによって、 置換することができます。

$template->param( varname  => 'こんにちは' );

これで <TMPL_VAR NAME=varname> の部分が「こんにちは」に置き換わります。

テンプレート変数の構文の大文字・小文字は区別されません。 <TMPL_VAR NAME=varname><tmpl_var name=varname> も同じに扱われます。 また、varname の部分もデフォルトでは大文字・小文字の区別はありません。 <TMPL_VAR NAME=varname><TMPL_VAR NAME=VARNAME> も期待通りに置換されます。

ただし、HTML::Template オブジェクト生成時にパラメータ case_sensitive に 1 を指定すると vername の部分は大文字・小文字が区別されます。

my $template = HTML::Template->new(
    case_sensitive => 1,
    ...
);

テンプレート変数の構文としては、HTML 構文と同様に、属性値をダブルクォーテーションで囲むこともできます。 つまり、<TMPL_VAR NAME="varname"> という記法も許されます。

HTML エスケープ

HTML::Template はテンプレート内の変数を置換する際に、HTML エスケープする機能を用意しています。 次のテンプレートとスクリプトを考えてみましょう。

連絡先:<TMPL_VAR NAME="contact">
$template->param( contact  => '田中太郎 <taro.tanaka@example.jp>' );

置換文字列の中に <> が含まれるため、HTML として出力すると期待通りにレンダリングされません。 置換文字列を HTML エスケープしないといけません。この場合、テンプレートでは次のように記述します。

連絡先:<TMPL_VAR NAME="contact" ESCAPE="html">

これによって、次のように出力されます。

連絡先:田中太郎 &lt;taro.tanaka@example.jp&gt;

デフォルトエスケープ

HTML エスケープするために、すべての変数の変換でテンプレートに <TMPL_VAR NAME="contact" ESCAPE="html"> と記述するのは面倒です。 そこで、デフォルトのエスケープを設定することで、ESCAPE="html" なしに HTML エスケープすることができるようになります。

HTML::Template オブジェクト生成時に default_escape => 'html' を指定します。

my $template = HTML::Template->new(
    ...
    default_escape => 'html',
);

Vanguard 互換モード

Vanguard 互換モードを有効にすると、<TMPL_VAR NAME="varname"> というテンプレート記述を %varname% という記述に簡素化できます。 Vanguard 互換モードを有効にするには、HTML::Template オブジェクト生成時にパラメータ vanguard_compatibility_mode1 をセットします。

my $template = HTML::Template->new(
    ...,
    vanguard_compatibility_mode => 1,
);

一見便利そうに見える Vanguard 互換モードですが、2 点注意が必要です。

一つ目は、Vanguard 互換モードを有効にすると、自動的にパラメータ die_on_bad_params0 がセットされます。たとえ明示的に die_on_bad_params1 をセットしても、それは無効になります。 これにより、もしテンプレートに記述されていない変数を Perl スクリプト側でセットしてもエラーにはなりません。 つまり、Perl スクリプト側で変数名を間違えても、気付きにくくなりますので注意が必要です。

二つ目は、Vanguard 互換モードを有効にすると、HTML エスケープをテンプレート側で指示できなくなります。 デフォルトモードであれば <TMPL_VAR NAME="contact" ESCAPE="html"> と記述することで HTML エスケープをテンプレート側で指示できました。 Vanguard 互換モードを使うなら、Perl スクリプト側で事前に HTML エスケープするか、または、 HTML::Template オブジェクト生成時に default_escape => 'html' を指定するのが良いでしょう。

条件分岐

HTML::Template のテンプレート構文で条件分岐には TMPL_IF, TMPL_ELSE, TMPL_UNLESS が用意されています。

<TMPL_IF NAME="count">
    count が 0 でなければ表示されます。
</TMPL_IF>

<TMPL_IF NAME="count">
    count が 0 でなければ表示されます。
<TMPL_ELSE>
    count が 0 なら表示されます。
</TMPL_IF>

<TMPL_UNLESS NAME="count">
   count が 0 なら表示されます。
</TMPL_UNLESS>

<TMPL_UNLESS NAME="count">
    count が 0 なら表示されます。
<TMPL_ELSE>
    count が 0 でなければ表示されます。
</TMPL_UNLESS>

TMPL_IF, TMPL_ELSE, TMPL_UNLESS は、 Perl の構文の if, else, unless と同じです。 変数 count の値が "string", 1 などの値であれば真、 "" (空文字列) や 0 などの値であれば偽として扱われます。

なお、Perl 構文であれば elsif がありますが、 HTML::Template 構文にはそれに相当する制御タグはありませんので注意してください。

繰り返し処理

構文

HTML::Template のテンプレート構文で繰り返し処理には TMPL_LOOP を使います。

<ul>
    <TMPL_LOOP NAME="mlist">
        <li><TMPL_VAR NAME="name" ESCAPE="html"> (<TMPL_VAR NAME="age">)</li>
    </TMPL_LOOP>
</ul>

Perl スクリプトでは、arrayref を与えます。

my $mlist = [ { name => '太郎', age => 24 }, { name => '次郎', age => 21 } ];
$template->param( mlist => $mlist );

arrayref (ここでは変数 $mlist) の各要素は hashref でなければいけない点に注意してください。 このサンプルコードは次のような結果を出力します。

<ul>

        <li>太郎 (24)</li>

        <li>次郎 (21)</li>

</ul>

変数のスコープ

HTML::Template では、テンプレート変数にもスコープという概念があります。 次の HTML テンプレート構文を見てみましょう。

<TMPL_LOOP NAME="product_list">
    <a href="%product_path%/%product_code%.html">%product_name%</a>
</TMPL_LOOP>

ここでテンプレート変数 %product_path% に注目してください。 この変数の値は、この繰り返しですべて同じ値だとします。 恐らく、Perl スクリプトを書くなら、このように書きたくなるのではないでしょうか。

my $product_path = '/products';
my $product_list = [
    { product_name => '商品A', product_code => 'A' },
    { product_name => '商品B', product_code => 'B' }
];
$template->param( product_path => $product_path );
$template->param( product_list => $product_list );

しかし、結果は期待通りにはなりません。%product_path% が置換されずに結果が出力されてしまいます。

    <a href="/A.html">商品A</a>

    <a href="/B.html">商品B</a>

HTML::Template のデフォルトの挙動では、TMPL_LOOP にセットした arrayref の外で定義された変数の値は、 TMPL_LOOP の中では使えません。 この点は、Perl の変数のスコープと異なりますので、違和感を感じますね。

もし TMPL_LOOP にセットした arrayref の外で定義された変数の値を TMPL_LOOP の中でも利用できるようにするには、 HTML::Template オブジェクト生成時でパラメータ global_vars1 を指定します。

my $template = HTML::Template->new(
    ...,
    global_vars => 1
);

これにより、結果は次のようになります。

    <a href="/products/A.html">商品A</a>

    <a href="/products/B.html">商品B</a>

ループコンテキスト変数

HTML::Template オブジェクトを生成する際に、パラメータ loop_context_vars1 を指定すると、TMPL_LOOP の中でループコンテキスト変数を利用することができます。

my $template = HTML::Template->new(
    ...,
    loop_context_vars => 1
);

ループコンテキスト変数は繰り返し処理に特化したプリセットの変数で、Perl スクリプトで定義する必要はありません。 HTML::Template で使えるループコンテキスト変数は以下の通りです。

__first__

繰り返しの初回だけ真の値を取り、それ以外では偽の値を取ります。

__last__

繰り返しの最後だけ真の値を取り、それ以外では偽の値を取ります。

__inner__

繰り返しの最初と最後を除いたときに真の値を取り、それ以外では偽の値を取ります。

__outer__

繰り返しの最初と最後のときに真の値を取り、それ以外では偽の値を取ります。

__odd__

繰り返しの奇数番目のときに真の値を取り、それ以外では偽の値を撮ります。

__even__

繰り返しの偶数番目のときに真の値を取り、それ以外では偽の値を撮ります。

__counter__

繰り返しの項目インデックス番号 (1 からスタート) がセットされます。

__index__

繰り返しの項目インデックス番号 (0 からスタート) がセットされます。

次のテンプレートをご覧ください。

<TMPL_LOOP NAME="product_list">
    %__counter__%. %product_name%<TMPL_IF NAME="__last__"> (新製品)</TMPL_IF>
</TMPL_LOOP>

このテンプレートはループの各項目の冒頭に 1 から順に番号を振ります。 そして、最後だけ (新商品) を付け加えます。 これを実現する Perl スクリプトは次の通りです。

my $template = HTML::Template->new(
    ...,
    loop_context_vars => 1
);

my $product_list = [
    { product_name => '商品A' },
    { product_name => '商品B' },
    { product_name => '商品C' }
];
$template->param( product_list => $product_list );

このサンプルコードを実行すると、次のような結果が出力されます。

    1. 商品A

    2. 商品B

    3. 商品C (新製品)

テンプレートファイルの組み込み

HTML::Template は、テンプレートファイルの中に別のテンプレートファイルを組み込むことができます。 この組み込みには TMPL_INCLUDE を使います。

<TMPL_INCLUDE NAME="header.html" />

このテンプレートは、header.html を組み込むことになりますが、 header.html は親となるテンプレートファイルと同じディレクトリにおきます。

組み込み HTML テンプレートファイルのみを下位ディレクトリに設置したい場合は、 このように記述することができます。

<TMPL_INCLUDE NAME="includes/header.html" />

これは親のテンプレートファイルから見た相対パス (includes/header.html) で組み込みたいファイルを指定しています。 しかし、子ディレクトリと言え、HTML::Template オブジェクト生成時に指定した検索パスに含まれていないと header.html は見つけられません。 そのため、保管するディレクトリを分けたい場合は、次のようにパラメータ path にパスを加えます。

my $template = HTML::Template->new(
    filename => 'template.html',
    path     => ["$FindBin::Bin", "$FindBin::Bin/includes"],
    ...
);

まとめ

以上で解説した通り、HTML::Template は軽量とは言え必要十分な機能が用意されています。 私も長年にわたり HTML::Template にはお世話になってきました。 近年では Template Toolkit を使うことが増えてきましたが、今なお、 新規プロジェクトでは検討の対象になるほどの利便性を備えています。

HTML::Template は何年にもわたり更新がなく、採用の際には心配になるかもしれませんが、 それほどに成熟し使い込まれた Perl モジュールとも言えます。 もしテンプレートエンジンの選択をすることがありましたら、この記事が参考になれば幸いです。