めちゃくちゃ忙しくてこのところブログをエスケープ気味でした。
どうやったらこの忙しさから解放されるのかばかり考えて時間だけが経っていますがそんなことはどうでもいいか

いきなりですが、PHPから使えるSmartyという「テンプレートエンジン」と呼ばれるものがあります。
このSmarty、割とPHP開発の現場では広く使われているようです。(私調べ)

PHP自体が既にテンプレートエンジンの性質を持っているのでなぜなぜどしてどしてそんなにスマーティーなの?
以前からこの辺ナゾでしたが、今日は遅ればせながらSmartyについて私なりに調べまくってついでにマスターしたいと思います。

よくSmartyの解説で書かれていることは、デザイナーとプログラマーの仕事の分担がうまくいくからみたいなことを目にしますがその辺どうなんでしょうか?


というわけでまずはSmarty導入から


Smartyは生ものなのでとにかく活きのいいやつを公式から生け捕ってきます。

最新の安定版(Latest Stable)なやつが狙い目ですね。
現在はSmarty 3.0.7がそれになります。


落としてきたやつを解凍しましょ
# tar zxvf Smarty-3.0.7.tar.gz

ZIPファイルなら
# unzip Smarty-3.0.7.zip


そこにできたSmarty-3.0.7/libsに必要なファイルが詰まっています。
Smarty-3.0.7/libs/Smarty.class.phpこのファイルに記述されたSmartyクラスを呼び出してオブジェクト作成して操作していくことになります。
Smarty-3.0.7だと冗長なのでsmartyにリネームしてこんなファイル配置にしてみました。

htdocs/ Web公開
    +- smarty/
    |    +-- libs/
    |    |    +-- Smarty.class.php
    |    +-- cache/
    |    +-- configs/
    |    +-- templates/
    |    |    +-- hello.tpl
    |    +-- templates_c/
    +- hello.php ...テスト用PHP


「cache」「configs」「templates」「templates_c」4つのディレクトリを作成して、テスト用にhello.tplとhello.phpを作成しました。

「cache」と「configs」は使わない場合もありますが一応作成しておくのさ

「cache」と「templates_c」は動的にファイル作成される領域なのでUNIX系の環境ならApacheユーザが読み書き実行できるよう権限を付与しましょ

# chmod 777 cache templates_c


smarty以下をWeb非公開にしたい場合はhtdocsなどのドキュメントルート外に出すといいのさ


ここまででインストールから設定までが完了しました!


あとはsmarty/demoを見るもよし、公式リファレンスを見るもよし、解説サイトを漁るもよし
一通りザクっと見たらだいたいの感じはつかめると思います。
覚えるポイントもさほど多くないのでこういったところも広く使われてる理由なんでしょうかね。



■まずは簡単な処理の流れを追うのだ

☆テンプレート側 [templates/hello.tpl]
<html>
<body>

<h1>Smarty Test</h1>
{$keyword}

</body>
</html>


テンプレートは拡張子 .tpl になります。(決まりではないですが、明確に区分けする意味で広く使われてるようです)
.htmlの方がブラウザで開いて確認しやすいとかなら.htmlでもいいかと思いますが、できるだけ王道でいきましょ

で、目的は{$keyword}部分を以下のスクリプト側でセットした値に置き換えて表示させることにあります。
{$keyword}のような記述方法については後述します。


☆スクリプト側 [htdocs/hello.php]
<?php
// Smartyクラス呼び出し
require_once './smarty/libs/Smarty.class.php';

// タイムゾーン設定
date_default_timezone_set('Asia/Tokyo');

// オブジェクト作成して必要な設定を行う
$smarty = new Smarty();

$smarty->template_dir = './smarty/templates/';
$smarty->compile_dir = './smarty/templates_c/';
$smarty->config_dir = './smarty/configs/';
$smarty->cache_dir = './smarty/cache/';

// テンプレート変数をセット
$smarty->assign("keyword", "Hello world!");

// テンプレートを指定して描画・表示
$smarty->display('hello.tpl');


HTTPリクエストを受けたhello.phpというファイルから見た Smarty.class.phpファイル呼び出しになるので hello.phpがいる場所(カレントディレクトリ)はドットで表現できます。
このドットの実体はリンクファイルで、今いるディレクトリにリンクしています。
なので './smarty/libs/Smarty.class.php' と書くことができます。

templates_dirなどのプロパティ設定も同じく、リクエストを受けたhello.phpから見た位置を設定します。

順番逆になりますが、date_default_timezone_set()を使ってタイムゾーンを設定しています。
これはxamppを使ってるとタイムゾーン設定がされてなかったりするのでPHP5.1.0以降で警告が出てしまうのを防ぐためです。
日頃からこれを書く癖を付けておいた方が手堅いでしょう。

assign()メソッドを使って、テンプレート側に渡す変数とその値を割り当てて送り出します。
assign()を使って指定した"keyword"をテンプレート側(hello.tpl)で変数$keywordとして使えることになります。
テンプレート側の名前空間に注入できるわけです。

最後にdisplay()メソッドを使って指定したテンプレートが処理され、最終的なHTMLページとして描画されレスポンスされることになります。

つまりSmartyオブジェクトを作成し、テンプレート等の置き場所を教えて、assign()でテンプレート変数とその値を割り当てて、最後にdisplay()メソッドにテンプレートを指定する流れになります。

従って、テンプレート等の置き場所を教える記述までは固定で別ファイルにくくり出しておいた方が後々便利に使いまわせるでしょう。


これでパス設定が間違っていなければ目的どおり表示されることになります。



■テンプレート記述方法

すでに {$foo}と書いた部分が、スクリプト側assign("foo", "値")でセットされたものに置き換わることは分かりました。
この他にもいろいろ便利なものがあります。

で、こんなにたくさんの記述方法があるよ~
と羅列したいところですが、大原則としてテンプレートが読みづらくなるようなごちゃごちゃとした記述になってしまっては本末転倒です。

どれを使うか吟味して必要最小限にした方がいいかも知れません。

テンプレートのすべてにおいて、{ と } に囲まれた部分がSmartyの処理する部分であることを意味しています。(デリミタと呼ぶ)
このデリミタ文字は設定変更で別のものにできますが、下手な小細工するより王道を使うべきでしょう。
なのでSmartyで処理させる部分以外のところで { や } があればエラーとなります。

シフトJISでテンプレートを書くと意図しないデリミタ文字が混入してエラーに悩まされることになってしまいます。
これも第2バイト目にアスキーコード割り当てが多いシフトJISの宿命です。
例えば「須」や「本」や「閲」などよく使いそうな文字が該当します。
でもデリミタ文字を変更する以外にも回避方法はあります。後述している{literal}~{/literal}でサンドイッチすれば回避できます。
{literal}必須本を閲覧{/literal}


☆配列やオブジェクトを扱う
{$array[0]}
{$hash.key}
{$hash.$key} 変数にセットされたキーを使える
{$obj->property}
{$obj->method()} メソッドの戻り値が表示される
{$obj->method($param)}


☆コメント文を書く
{* コメント文字 *}


ここまでは変数およびコメント記述だったので値を表示させるかコメントにするかというシンプルな振る舞いでした。
以降はSmarty独自の組み込み関数を使ったより複雑な振る舞いになります。

☆囲んだ範囲をデリミタと見なさないようにする方法
{literal}
JavaScriptソースなど
{/literal}


☆他ファイル呼び出し
{include file='header.tpl'}


ヘッダー・フッターなど、テンプレートを更にパーツ化してテンプレート間で共有したり可読性を高めたりするのに便利です。

また、ナイスな機能として下記のようにすれば呼び出すテンプレート先で変数として使うことができます。
{* header.tpl内で$titleや$fooを使えます *}
{include file='header.tpl' title='タイトル' foo=$val}


☆設定ファイルの値を取得する方法
ここで今までディレクトリ指定だけして使っていなかったconfigsディレクトリの出番です。
-----configs/test.conf
title = 'Foo Bar'


-----templates/test.tpl
{* configs/test.confの呼び出し *}
{config_load file='test.conf'}

{* 設定値は変数と違って #title# という書き方 *}
<tag>{#title#}</tag>


もう1パターン書いておきます。
設定ファイルのセクション分けができることを利用して複数ページ分の設定を並べてわかりやすくすることができます。
------configs/test.conf
[Section_Page_Foo]
title = 'Fooページのタイトル'
message = 'Fooページのメッセージ'

[Section_Page_Bar]
title = 'Barページのタイトル'
message = 'Barページのメッセージ'


-----templates/test.tpl
{* configs/test.confの呼び出し *}
{config_load file='test.conf' section='Section_Page_Bar'}

{* 設定値は変数と違って #foo# 形式とする *}
<h1>{#title#}</h1>
<p>{#message#}</p>


この他、{if 条件}~{/if}のような制御構造や{foreach パラメータ}~{/foreach}のようなループなどを擬似的に行える関数が用意されていますが、テンプレートをシンプルに保つためにも極力使うべきではない気がします。

{html_image file='foo.gif'}で<img src="foo.gif" width="" height="">を描画させるなんてもってのほかです。
テンプレートに<img src="foo.gif">を書いてあったほうが見やすいはずです。


スクリプト側で定義した定数を、テンプレート側で予約変数$smartyを使って参照することができます。
------htdocs/test.php
<?php
   :
define("INPUT_NAME", "foobar_name");
   :


------templates/test.tpl
<input type="text" name="{$smarty.const.INPUT_NAME}" value="{$smarty.post[$smarty.const.INPUT_NAME]|escape}">


でもこれだとフォーム要素のname値としてテンプレートを見ただけではどういった値をやり取りしてるのかがわからなくなってしまいます。
なのでこの場合も<input type="text" name="foobar_name" value="{$foobar_name|escape}">このように必要な情報をハードコーディングした方が見通しがよさそうです。

つまりスクリプト側で定数定義してテンプレート側まで一元管理しようとするとせっかくの役割分担が台無しといったところでしょうか。。。


☆修飾子を使う
テンプレート記述として変数・コメント・関数が終わりました。今度は修飾子と呼ばれるものです。
{$val|upper}と記述すれば、変数$valの値に英小文字があれば英大文字に変換された表示になります。
つまりフィルターをかけた形になります。

ちょうどUNIXコマンドで標準出力を|パイプでつないで次のコマンドへの標準入力にして処理をつなげるようなイメージになります。
これだけ処理記述が近接してるので見通しがよくわかりやすいものだと思います。

「<⇒&lt;」などのようないわゆるサニタイズは
{$val|escape} と書くだけで処理されることになります。
つまり入力された値はこうしておけばXSS攻撃を防げるわけです。

その他では改行を<br />タグに変換するものなどいろいろあります。
{$val|escape|nl2br}


以上で簡単なテンプレート記述法は終わりです。


■Smarty使用パターン(その1)
どんな使い方が王道なのか思うままに書いておきます。

☆テンプレートの分け方
-----templates/template.tpl
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="{#stylesheet#}" type="text/css" />
<title>共通タイトル{#subTitle#}</title>
</head>
<body>

<div id="container">
    <div id="header">
        {include file='header.tpl'}
    </div>
    <div id="leftcolumn">
        {include file='left.tpl'}
    </div>
    <div id="maincolumn">
        {include file='main.tpl' main=$main}
    </div>

    <br class="clr" />
    <div id="footer">
        {include file='footer.tpl'}
    </div>

</div>

</body>
</html>


外側とレイアウトの骨格部分だけの抜け殻テンプレートを全ページ共通用に1つだけ用意してあとはパーツ用テンプレートに分けると管理しやすくなりそうです。
サイトによってはヘッダー・フッターも全ページ共通でいけるケースもあるのでここに埋め込んでもよさそうです。
テンプレート側を受け持つ人は、どんな変数が渡されてくるかだけ気にすればあとはデザインに集中できるでしょう。


-----templates/header.tpl
<h1>{#subTitle#}</h1>


このように設定ファイルを使ってセットされた値は、テンプレートから呼び出されたテンプレート内でもグローバルに使えるので変数を使って渡す手間が要らない。


☆スクリプト側
------htdocs/foo.php
<?php
// 自前のSmartyオブジェクト作成関数呼び出し
require_once 'path/to/smarty.php';

// Smartyオブジェクト作成・取得
$smarty = smarty();

// 処理振り分け
switch (req('process')) {
    case "foo":
        $section = 'foo';
        fooMain($smarty);
        break;

    case "bar":
        $section = 'bar';
        barMain($smarty);
        break;

default:
        $section = 'fuga';
        fugaMain($smarty);
        break;

}

// 設定呼び出し
$smarty->config_load('common.conf', $section);

// 描画・レスポンス
$smarty->display("template.tpl");

function req($key) {
    return isset($_REQUEST[$key]) ? $_REQUEST[$key] : '';
}
function fooMain($smarty) {
    //fooのメイン処理。テンプレートに渡す値あればassign
    //$smarty->assign("main", "メインコンテンツ");
}
function barMain($smarty) { ... }
function fugaMain($smarty) { ... }


ここで問題発生!

config_load()メソッドを使ったら下記の注意文が表示されてしまいました。
なので代わりにconfigLoad()メソッドを使うようにした方がいいようです。

注意文:
Notice: function call 'config_load' is unknown or deprecated



☆設定ファイル
-----configs/common.conf
subTitle = "くつ下を左だけ履く友の会"
footer = "2011 (C) コピーライト的なもの"


長々となってしまいましたが、こんな感じで書けるっしょ
めげないでパターン2を書くっしょ



■Smarty使用パターン(その2)

よくあるパターンですが、入力を検証してOKなら次画面へ遷移して、NGなら元の画面に入力値とエラーメッセージを表示させるものです。

☆スクリプト側
-----htdocs/input.php
<?php
// 自前のSmartyオブジェクト作成関数呼び出し
require_once 'path/to/smarty.php';

// Smartyオブジェクト作成・取得
$smarty = smarty();

// 入力値をテンプレート変数にセット
$smarty->assign("input", $_POST);

// 検証結果で処理を振り分け
$ERRORS = array();
if (is_valid()) { // 検証OKなら

    $section = 'confirm'; // 確認画面用セクション
    $template = 'confirm.tpl';
} else {

    $smarty->assign("errors", $ERRORS); // エラー内容渡す
    $section = 'input'; // 元のまま
    $template = 'input.tpl';
}

// 設定呼び出し
$smarty->configLoad('common.conf', $section);

// 描画・レスポンス
$smarty->display($template);


function req($key) {
    return isset($_POST[$key]) ? $_POST[$key] : '';
}
function is_valid() {
    global $ERRORS;

    // あれこれ検証して最後に真偽値を返します。
    // また、エラーがあればその内容をセットします。
    // クラス化していればプロパティにエラーをセットできますが、していなければグローバル変数にセットするようにします。
}


☆テンプレート側
-----templates/input.tpl
<html>
<head>
<style type="text/css">
.err{color:red}
</style>
</head>
<body>

<form action="input.php" method="post">
<input type="text" name="foo" value="{$input.foo|escape}"><b class=err>{$errors.foo}</b>
<input type="submit" value="送信">
</form>

</body>
</html>


こう書くと、検証NGのときに再び同じ画面を使って入力値をサニタイズしてセットする記述とエラーメッセージを表示させる記述がわかりやすくていいと思います。
こういうところもPHP開発の現場でSmartyが広く使われている理由のような気がします。

そういうのもあって、<select>のような選択系も検証NGのときに選択値に戻す必要があります。
これはどう書けばいいんでしょ
<select name="select_foo">
<option value="1" {if $form.select_foo == 1}selected="selected"{/if}>foo</option>
     :
</select>


こんなことをoptionの数記述しようものなら可読性落ちます。
こういう場合はhtml_optionsというカスタム関数を使う方がよさそうです。
{html_options name='select_foo' options=$form.select_foo_list selected=$form.select_foo}



■Smarty使用パターン(番外)

最後にちょっとしたおつまみ程度ですが、描画結果を一旦取得してなんやらかんやらしたい場合があります。
描画ページに対して文字コード変換やフィルターを最後にかけたい場合などです。
こういう場合はこれまでスクリプトのおしりの方で使ってきたdisplay()メソッドの代わりにfetch()メソッドを使って取得できます。
あとは思うがままです。
$html = $smarty->fetch("template.tpl");
echo preg_replace('#<body.*</body>#is', '<body>これがSmartyじゃーい!</body>', $html);



■Smartyに関する私的感想
・導入が簡単
・シンプルなので割と覚えやすい。
・テンプレートのフォーム要素にエスケープやフィルタリング等をコンパクトに指定できるので見慣れたら直感的に把握しやすくなる。
・以上のことが、グループ開発を行うのに適している。

注意点として、テンプレートの可読性が落ちないように配慮する必要がある。Smartyをスマーティーに保つのは自分しだいってことですね。