前回Smartyの基本を学んだので今度はWebアプリケーションと呼べるものを作ってみたいと思います。

簡単なアンケートを入力送信してもらい、確認画面で戻ってやり直すか確定させるかという処理になります。
もし入力に不備があればエラーメッセージを表示させて再入力を求めることになります。

まずはファイル配置はこんな感じです。


■ファイル配置
htdocs/
    +- smarty/
    |    +- cache/
    |    +- configs/
    |    |    +- common.conf
    |    |
    |    +- templates/
    |    |    +- akt.tpl
    |    |    +- confirm.tpl
    |    |    +- result.tpl
    |    |    +- template.tpl
    |    |
    |    +- templates_c/
    |    +- ExtSmarty.php
    |    +- Smarty.class.php
    |
    +- akt.php
    +- common.php

例にならって、動的にファイル作成され得る「cache」「templates_c」の2ディレクトリにはApacheユーザの読み書き実行権限を許可する設定となります。

今回はメインとなるアンケート処理をakt.phpに、共通の定数定義をcommon.phpに分けました。
共通とは言ってもスクリプトはakt.phpのみなので、もし他にスクリプトがあった場合やバッチ処理などあれば共通に呼び出せる定義という意味になります。

Smartyクラスを継承したExtSmartyクラスを作ってみました。
クラスはどういう使い方ができれば便利なのか、使う人の側で考える必要があります。
今後も共通して使えるクラスを作る意味で、以下のことを考慮しました。

●Smarty処理の流れ
1.Smartyオブジェクト作成
2.各種ファイル置き場を指定
3.assign()メソッドでテンプレートに渡す変数をセット
4.display()メソッドに渡したテンプレートが描画&レスポンス

この流れにおいて2番までは固定で持てると考えるとコンストラクタに記述できそうです。
3番4番はページ毎に指定が違うということはありますが、テンプレート側に渡す必要のある変数をセットして最後に描画するという流れはどのページも毎回同じパターンと考えることができます。
そこでこの同パターンをGoFデザインパターンでいう所のテンプレートメソッドパターンにすることにします。


■以上のことを踏まえてExtSmartyクラスを作成してみました。

[smarty/ExtSmarty.php]
<?php
/**
* Smarty.class.phpと同じディレクトリに設置して呼び出す拡張クラス
*/
if (!defined("SMARTY_DIR")) {
    define("SMARTY_DIR", dirname(__FILE__) . '/');
}
require_once SMARTY_DIR . 'Smarty.class.php';
date_default_timezone_set('Asia/Tokyo');

class ExtSmarty extends Smarty
{
    const DATA = 'data';
    const ERROR = 'error';
    private $_data = array();
    private $_error = array();

    public function __construct($caching=0) {

        parent::__construct();

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

        if (is_int($caching) && $caching >= 10) {
            $this->caching = true;
            $this->cach_lifetime = $caching;
        }
    }
    public function setError($key, $errmsg='') {

        if (is_array($key)) {
            $this->_error = array_merge($this->_error, $key);
        } else {
            $this->_error[$key] = $errmsg;
        }
    }
    public function setData($key, $data='') {

        if (is_array($key)) {
            $this->_data = array_merge($this->_data, $key);
        } else {
            $this->_data[$key] = $data;
        }
    }
    public function hasError() {
        return !empty($this->_error);
    }
    public function extDisplay($tpl) {

        $this->assign(self::DATA, $this->_data);
        $this->assign(self::ERROR, $this->_error);
        $this->display($tpl);
    }

}

テンプレート側に渡す変数名として、"data" と "error" の2つを内部定義しています。
それぞれのキーと値のペアをプロパティに保持できるようにします。
クラス外部から直接プロパティにセットするのは行儀が悪いので、そのためのセッターを用意することにしました。

今回はアンケートなのでキャッシュさせるのは都合が悪いですが、今後使いまわすために一応
$obj = new ExtSmarty(300);

としてオブジェクト作成すれば300秒キャッシュされるようにしています。


■次は共通の定義ファイルです。
[htdocs/common.php]
<?php
/**
* 共通定義ファイル
*/
require_once './smarty/ExtSmarty.php';

;// アクション値
define("ACTION_AKT", "akt");
define("ACTION_CONFIRM", "confirm");
define("ACTION_RESULT", "result");

;// フォーム要素name属性名(=テンプレート変数キー)
define("FORM_INPUT1", "input1");
define("FORM_INPUT2", "input2");
define("FORM_INPUT3", "input3");
define("FORM_INPUT4", "input4");
define("FORM_INPUT5", "input5");

;// リスト構造を持つフォーム要素のテンプレート変数キー
define("FORM_INPUT2_LIST", "input2_list");
define("FORM_INPUT4_LIST", "input4_list");
define("FORM_INPUT5_LIST", "input5_list");

;// 最大入力データ長定義
define("MAX_FORM_INPUT1_LEN", 30);
define("MAX_FORM_INPUT3_LEN", 256);

;// エラーメッセージ定数定義
define("ERR_REQUIRED", "空っぽやん" );
define("ERR_NOSELECT", "選択してないやん" );
define("ERR_LONGEST" , "どう見ても長すぎるやろ");
define("ERR_NOLIST" , "選択肢にないやろ" );

;// 設定ファイル名定義
define("FILE_CONF", "common.conf");

;// リスト構造を持つフォーム要素のテンプレート変数値
$FORM_INPUT2_LIST = array(
    "key1" => "ラベル1",
    "key2" => "ラベル2",
    "key3" => "ラベル3"
);
$FORM_INPUT4_LIST = array(
    "key1" => "ラベル1",
    "key2" => "ラベル2",
    "key3" => "ラベル3"
);
$FORM_INPUT5_LIST = array(
    "key1" => "ラベル1",
    "key2" => "ラベル2",
    "key3" => "ラベル3"
);



■続いてはアンケート集計スクリプトです。

[htdocs/akt.php]
<?php
/**
* アンケート集計
*/
ini_set("display_errors", 1); error_reporting(E_ALL & ~E_NOTICE);
require_once './common.php';
$smarty = new ExtSmarty();

// 入力データの一時保管用にセッションを使う
session_start();

// 処理の振り分け
switch ($_POST['action']) {

    case ACTION_CONFIRM:
        confirm($smarty);
        break;

    case ACTION_RESULT:
        result($smarty);
        break;

    default:
        akt($smarty);
        break;
}


function akt($smarty) {

    $_POST = $_SESSION; // 保存データの戻し
    $_SESSION = array();

    // 設定ファイル呼び出し
    $smarty->configLoad(FILE_CONF, ACTION_AKT);

    // テンプレートへ渡す変数をセットして、描画&レスポンス
    _setInputData($smarty);

    // 描画・レスポンス
    $smarty->extDisplay($smarty->getConfigVars('tpl_display'));
}
function confirm($smarty) {

    $_SESSION = array(); // 初期化
    $_SESSION = $_POST; // サーバ側に保存

    if (is_valid($smarty)) { // 入力が妥当なら

        // 設定ファイル呼び出し
        $smarty->configLoad(FILE_CONF, ACTION_CONFIRM);

        // テンプレートへ渡す変数をセット
        _setInputData($smarty);

        // 描画・レスポンス
        $smarty->extDisplay($smarty->getConfigVars('tpl_display'));

    } else {
        akt($smarty); // 入力画面へ戻す
    }
}
function result($smarty) {

    if (!empty($_SESSION)) {
        // 実際にはここでアンケート集計処理を行う
    }
    // 処理が済んだら後始末
    $_SESSION = array();
    session_destroy();

    // 設定ファイル呼び出し
    $smarty->configLoad(FILE_CONF, ACTION_RESULT);

    // 描画・レスポンス
    $smarty->extDisplay($smarty->getConfigVars('tpl_display'));
}
function _setInputData($smarty) {

    global $FORM_INPUT2_LIST;
    global $FORM_INPUT4_LIST;
    global $FORM_INPUT5_LIST;

    // テンプレートへ渡す変数をセット
    $smarty->setData($_POST);
    $smarty->setData(FORM_INPUT2_LIST, $FORM_INPUT2_LIST);
    $smarty->setData(FORM_INPUT4_LIST, $FORM_INPUT4_LIST);
    $smarty->setData(FORM_INPUT5_LIST, $FORM_INPUT5_LIST);

}
function is_valid($smarty) {

    validationInput1($smarty);
    validationInput2($smarty);
    validationInput3($smarty);
    validationInput4($smarty);
    validationInput5($smarty);

    return !$smarty->hasError();
}
function validationInput1($smarty) {

    if (empty($_POST[FORM_INPUT1])) { // 入力の有無
        $smarty->setError(FORM_INPUT1, ERR_REQUIRED);
        return false;
    } else {
        if (strlen($_POST[FORM_INPUT1]) > MAX_FORM_INPUT1_LEN) { // データ長
            $smarty->setError(FORM_INPUT1, ERR_LONGEST);
            return false;
        }
        return true;
    }
}
function validationInput2($smarty) {
    global $FORM_INPUT2_LIST;

    if (empty($_POST[FORM_INPUT2])) {
        $smarty->setError(FORM_INPUT2, ERR_NOSELECT);
        return false;
    } else {
        if (!isset($FORM_INPUT2_LIST[$_POST[FORM_INPUT2]])) {
            $smarty->setError(FORM_INPUT2, ERR_NOLIST);
            return false;
        }
        return true;
    }
}
function validationInput3($smarty) {

    if (empty($_POST[FORM_INPUT3])) {
        $smarty->setError(FORM_INPUT3, ERR_REQUIRED);
        return false;
    } else {
        if (strlen($_POST[FORM_INPUT3]) > MAX_FORM_INPUT3_LEN) {
            $smarty->setError(FORM_INPUT3, ERR_LONGEST);
            return false;
        }
        return true;
    }
}
function validationInput4($smarty) {
    global $FORM_INPUT4_LIST;

    if (empty($_POST[FORM_INPUT4])) {
        $smarty->setError(FORM_INPUT4, ERR_NOSELECT);
        return false;
    } else {
        foreach ($_POST[FORM_INPUT4] as $value) {
            if (!isset($FORM_INPUT4_LIST[$value])) {
                $smarty->setError(FORM_INPUT4, ERR_NOLIST);
                return false;
            }
        }
        return true;
    }
}
function validationInput5($smarty) {
    global $FORM_INPUT5_LIST;

    if (empty($_POST[FORM_INPUT5])) {
        $smarty->setError(FORM_INPUT5, ERR_NOSELECT);
        return false;
    } else {
        if (!isset($FORM_INPUT5_LIST[$_POST[FORM_INPUT5]])) {
            $smarty->setError(FORM_INPUT5, ERR_NOLIST);
            return false;
        }
        return true;
    }
}

文字コードはなんでもいいですが、今回も例によってシフトJISを使っています。
出力画面もシフトJISなのでおおかた日本語が入力されれば同じシフトJISで受け取ることになります。
例外を考慮して真っ先に受け取ったデータの文字エンコーディングを判別して必ずシフトJISになるように前処理を施すやり方もありますが、今回は手抜きしています。

バリデーションは適当です。

error_reporting(E_ALL)だと未定義エラー出まくりなのでerror_reporting(E_ALL & ~E_NOTICE)にしています。
未定義エラーにも対応するには、isset()でチェックを入れる必要があり、もう少しコードが膨れることになります。

スクリプトはこんなところです。


■次は設定ファイルです。

[configs/common.conf]
##
# 設定ファイル
#

title = "アンケート"
footer = "2011 コピーライト的なものとか"
tpl_display = "template.tpl"

[akt]
subTitle = " - 入力画面"
tpl_contents = "akt.tpl"

[confirm]
subTitle = " - 確認画面"
tpl_contents = "confirm.tpl"

[result]
subTitle = " - 結果画面"
tpl_contents = "result.tpl"

一応、ページ間で共通の情報をくくり出したような設定値と、ページ毎の設定値です。
もしかしたら固定情報はテンプレートに直接書いておいた方が、テンプレートを見ただけでイメージしやすくなるので設定で持ちすぎるのは悪手かも知れません。
この辺りは研究の余地ありですね。


■そしてテンプレートですが、全部で4つ作りました。
画面数3つに対して、もう1つひな形となるテンプレートを用意しました。
デザイン・レイアウトはここに集約させて全体の共通感を出したいですね。

[templates/template.tpl]
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS" />
<title>{#title#}{#subTitle#}</title>
<style type="text/css">
{literal}
body{background-color:Beige}
h1{text-align:center}
dt{background-color:Khaki}
p{color:red;margin-top:0}
#footer{text-align:right; padding-right:20px}
.btn{width:100px}
{/literal}
</style>
</head>
<body>

{* 全ページ共通のひな形 *}

<h1>{#title#}</h1>
{include file=#tpl_contents# data=$data error=$error}

<hr />
<div id="footer">{#footer#}</div>

</body>
</html>

見てのとおり、テンプレートもソースコードもすべて禁断のシフトJISで書いています。
なので要所要所で{literal}~{/literal}を使ってエラー回避することを常に意識しておく必要があります。

外枠そのままで、中身を{include}ですげ替えます。
その際に$dataと$error 2つの変数を渡しています。


■入力フォーム画面部テンプレートです。
テキストボックスの記述は割と簡単なので、できるだけそれ以外のフォーム要素を使ってみました。

[templates/akt.tpl]
{**
* コンテンツ:入力フォーム
*}

<em>入力フォーム</em>


<form action="{$SCRIPT_NAME}" method="post">
<dl>
<dt>入力1</dt>
<dd><input type="text" name="input1" value="{$data.input1|escape:"html":"Shift_JIS"}" />
<p>{$error.input1}</p></dd>

<dt>入力2</dt>
<dd>{html_radios name="input2" options=$data.input2_list selected=$data.input2}
<p>{$error.input2}</p></dd>

<dt>入力3</dt>
<dd><textarea name="input3" rows="4" cols="30">{$data.input3|escape:"html":"Shift_JIS"}</textarea>
<p>{$error.input3}</p></dd>

<dt>入力4</dt>
<dd>{html_checkboxes name="input4" options=$data.input4_list selected=$data.input4}
<p>{$error.input4}</p></dd>

<dt>入力5</dt>
<dd>{html_options name="input5" options=$data.input5_list selected=$data.input5}
<p>{$error.input5}</p></dd>

</dl>
<input type="submit" value="送信" class="btn" />
<input type="hidden" name="action" value="confirm" />
</form>

$SCRIPT_NAMEは、Smartyクラス内部でセットされる変数で、$_SERVER['SCRIPT_NAME']値が代入されたものです。
Smartyクラスを読めばいろいろ使えそうなものが発見できると思います。

{html_radios}関数等を使わないでテキストボックスと同じく直接タグ記述してしまうと、エラーで再入力を求めるときにうまく初期値をセットする解決法が見つかりません。
ここは不本意ながらも泣く泣く{html_radios}関数等を使うことにしています。

{$foo|escape:"html":"Shift_JIS"}の記述をしないとシフトJIS画面から入力されたマルチバイト文字は表示段階でフィルターがかかって空っぽになってしまいます。

この中でチェックボックスだけが複数選択可能なので配列形式のデータが送られてくることになります。

{html_checkboxes name="input4"}のタグ描画はこんな感じ
<label><input type="checkbox" name="input4[]" value="key1" />ラベル1</label>
<label><input type="checkbox" name="input4[]" value="key2" />ラベル2</label>
<label><input type="checkbox" name="input4[]" value="key3" />ラベル3</label>

複数選択可能なものとしてこの他にはセレクトボックスのマルチプルなものがあります。


■確認画面部テンプレートです。

[templates/confirm.tpl]
{**
* コンテンツ:確認表示
*}

<em>これでいいのかい?</em>


<dl>
<dt>入力1</dt>
<dd>{$data.input1|escape:"html":"Shift_JIS"}
<p></p></dd>

<dt>入力2</dt>
<dd>{$data.input2_list[$data.input2]}
<p></p></dd>

<dt>入力3</dt>
<dd>{$data.input3|escape:"html":"Shift_JIS"|nl2br}
<p></p></dd>

<dt>入力4</dt>
<dd>
{foreach $data.input4 as $value}
{$data.input4_list[$value]}<br />
{/foreach}
<p></p></dd>

<dt>入力5</dt>
<dd>{$data.input5_list[$data.input5]}</dd>

</dl>

<table border="0">
<tr><td>
<form action="{$SCRIPT_NAME}" method="post">
<input type="submit" value="戻る" class="btn" />
<input type="hidden" name="action" value="akt" />
</form>
</td><td>
<form action="{$SCRIPT_NAME}" method="post">
<input type="submit" value="OK" class="btn" />
<input type="hidden" name="action" value="result" />
</form>
</td></tr>
</table>

テキストエリアには複数行入力される場合もあるので、表示段階ではescape修飾子でサニタイズした後に改行コードを<br />タグ置換するnl2br修飾子を使います。

チェックボックスは複数選択可能につき配列型なので、泣く泣く{foreach}関数を使って表示させています。
もっと明快でスッキリした記述があればいいんですが、この辺も今後の課題です。


■最後に、目的の処理が済んだ後の単なる結果表示部テンプレートになります。

[templates/result.tpl]
{**
* コンテンツ:結果表示
*}

<em>結果メッセージ</em>


<h3>ご協力ありがとうございました。</h3>

<form action="/" method="get">
<input type="submit" value="トップへ" class="btn" />
</form>

ここでは特にやり取りするような変数等はありません。



画面はこんな感じになります。

〓入力画面〓

そろそろホンキ出す-初期入力画面



〓エラー画面〓

そろそろホンキ出す-エラー表示画面



〓確認画面〓

そろそろホンキ出す-確認画面



〓結果画面〓

そろそろホンキ出す-結果画面