このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

サブテンプレート処理について考える

Catyのテンプレート処理について再度考えてみたのですが、ある程度一般論っぽい書き方で述べます。関数呼び出しの記法を全面的に使います。

内容:

  1. トップレベルのテンプレート処理
  2. サブテンプレートの処理
  3. サブテンプレートに渡す情報
  4. サブテンプレートに入力とパラメータを渡す
  5. テンプレート処理を一元化したかった理由
  6. それで何ができるか

トップレベルのテンプレート処理

テンプレートテキストtemplがあるとき、コンテキスト(名前-値ペアの集まり)contextを使ってテンプレートテキストを展開することを expand(context, templ) と書きましょう。この関数呼び出しの値がテンプレート展開結果です。contextが単なるデータではつまらないので、プログラムの実行結果だとします。ここでは、式exprを評価した値 eval(expr) がコンテキストになるとします。そうすると、次がテンプレート展開結果です。

  • expand(eval(expr), templ)

evalをもう少し精密化して、入力inputとパラメータpramsにも依存して評価するとしましょう。inputもparamsも要するに外部から渡されるデータってことでして、ひとつにまとめてもいいのですが、便宜上2つにしておきます。そうすると、テンプレート展開は次の形になります。

  • expand(eval(input, expr, params), templ)

上の式をresolveという関数にまとめます。

  • resolve(input, expr, params, templ) := expand(eval(input, expr, params), templ)

ここで、templとexprは、データとしては単なるテキストです。これらのテキストはファイルシステムに保存されているとします。expr_path, templ_path はファイルシステムのパス(文字列)として、テキスト自体の代わりに、ファイルのパスを引数にしましょう。すると*1

  • resolve_path(input, expr_path, params, templ_path) := resolve(input, read(expr_path), params, read(templ_path))

さらに、templ_pathとexpar_pathに法則的な関係があって、templ_pathさえあればexpr_pathが分かるとします。これを使って書き換えると:

  • resolve_path_2(input, params, templ_path) := resolve_path(input, read_expr(templ_path), params, read(templ_path))

このresolve_path_2をWeb処理だとみなしましょう。つまり、次のように解釈します。

  1. input : POSTデータ
  2. params : クエリー文字列
  3. templ_path : テンプレートファイルを参照するURL

それらしい名前に書き換えてみると:

  • process_request(post_data, url_path, query_params)

もう一度定義を振り返れば:


process_request(post_data, url_path, query_params)
:= expand(eval(post_data, read_expr(url_path), query_params),
read(url_path))

サブテンプレートの処理

テレンス・パーが言っている「テンプレートの最小機能4つ」のなかに、サブテンプレート機能があります*2 -- これは、テンプレート中に別なテンプレートの展開結果を挿入できるという機能です。

サブテンプレートであっても、expand(eval(input, expr, params), templ) で展開することに変わりはありません。ただし、現状のCatyでは、inputやparamsが使えないので、とりあえず expand(eval(expr), templ) から考えます。サブテンプレートもファイルに格納されているのでパス名 templ_path を持ちます。評価すべき式がtempl_pathから得られるという仮定も同じなので:

  • expand(eval(read_expr(templ_path)), read(templ))

となります。これだけではトップレベルの展開と変わりませんが、実は、サブテンプレートの場合は親の存在を無視できないのです。どういうことかと言うと、親のコンテストが子(サブテンプレート)にも適用されるのです。コンテキストのマージを context1 +< context2 のように、足し算記号と不等号を組み合わせて書くことします。不等号は「大きい方が優先する」という意味で使います。今の例では、context2 を優先するので、context1 にある同じ名前の変数は隠されます。

親のコンテキストも影響することを書き下すと:

  • expand(parent_context +< eval(read_expr(templ_path)), read(templ))

これだけでもほとんどのサブテンプレート処理は十分です。が、サブテンプレート処理のほうが少し機能が貧弱になっています。この点を以下では問題にします。

サブテンプレートに渡す情報

今までに話した方法では、親から子(サブテンプレート)に渡る情報はコンテキストだけです。そのコンテキストも、子供の側で生成したコンテキストより弱い形でしか影響しません。親が子に影響を与える手段が限られています。

まず、コンテキトで親が子に影響を与える方法を少し拡張します。サブテンプレートを呼び出す(インクルードする)時点で、コンテキストの一部をセットできると便利でしょう。Smartyでは次のように書けます。


{include file="header.tpl" greeting="いらっしゃいませ。"}

ここで、header.tplサブテンプレートのパス名です。greetingの指定はコンテキストとして子供に渡っていくことになります。include指令(テンプレートタグ)のパラメータ(属性)であるfileと、子供に渡すパラメータの構文が同じなのがどうにもイヤです。次のような構文のほうが僕は安心です。


{include file="header.tpl"}
{context name="greeting" value="いらっしゃいま"}
{/include}

まーしかし、「書くのが面倒」とか「互換性がない」とかの理由で、気持ち悪くてもSmarty構文にするのが現実的なのかもしれません。

構文の問題をさておけば、コンテキストを子供に渡す方法はこれでいいとします。あとは、式評価(eval)のときのinputとparamsです。

サブテンプレートに入力とパラメータを渡す

入力(input)とパラメータ(params)のなかで、パラメータはパス名に付加してしまっていいと思います。つまり、クエリ文字列方式です。


{include file="header.tpl?name=hiyama"}

これによって、evalが利用するパラメータ {"name": "hiyama"} が得られます。Webからのリクエストのときとまったく同じ方式なので統一性もあるかと。

最後に残ったinputに関しては、includeテンプレートタグが持つ属性として "input" とか "data" とかを追加するしかなさそうです。


{include file="listCart.tpl" input=$cartInfo}

テンプレート処理を一元化したかった理由

以上のようにすると、トップレベルのテンプレート処理とサブテンプレートの処理の区別がなくなります。最近まで僕は、サブテンプレートはinclude経由でしかアクセスされないのだから、トップレベルに比べて簡略な処理でもいいと思っていました。しかし、どんなテンプレートファイルでも、次の3つの方法でアクセスされる可能性があります。

  1. Webからの通常のリクエスト(GETまたはPOST)
  2. 親テンプレートからのinclude
  3. AjaxXMLHttpRequest)通信のリクエスト(GETまたはPOST)

Ajaxによるリクエストは、include処理をリモート化したようなもので、トップレベル・テンプレート処理とサブテンプレート処理の中間の(あるいは混じった)ような性格を持ちます。リクエストされる状況が増えるに従い処理方式も増やすと複雑化するので、リクエスト状況のことは考えないで、どんなときでも同じ方法で処理するほうがシンプルになるでしょう。

というわけで、テンプレート処理は一律に次の関数で表現できます。


expand(parent_context +<
eval(input, read_expr(templ_path), params),
read(templ_path))

トップレベル・テンプレート処理のときは、parent_contextが空({})になるだけです。

それで何ができるか

テンプレート処理のincludeは、サーバ側で処理が完了し、出来上がった結果がブラウザに送られます。Ajaxを使うと、includeのタイミングを変える事ができて、ブラウザに届いた後でリモートからinclude処理を依頼することができます。テッド・ネルソンの言葉を借りればトランスクルード(transclude)と言えるでしょう。

カニズム的にインクルードとトランスクルードを別々に扱っているのはカッコ悪いし、弊害がありそうなので、トップレベルのアクセス/インクルード・アクセス/トランスクルード・アクセスのサーバー側処理を全部同じにしておきたい、という話です。

*1:あれっ、今気がついたけど、resolve path ってネームサーバーみたいな印象がありますね。確かに、テンプレートのパスからスクリプトのパスを引き当てる点ではネームサービスっぽいですが、resolveの意味としては(英辞郎から):「〔〜を別のものに〕変化させる、転換させる」というあたりが僕の意図です。

*2:他の3つは、テンプレート変数、if文、foreach文です。