ここまでで以下の流れを実装できました。
入力値のチェック形式は、$TableStructに詰め込んだ連想配列の
"dcf" => _DCF_NOT_NULL | _DCF_NUMBER,
この要素で指示できます。組み合わせられるチェック形式は
_DCF_NOT_NULL, _DCF_NUMBER, _DCF_ZENKAKU, _DCF_DATE, _DCF_EISUU,
_DCF_KANA, _DCF_MAIL, _DCF_UNIQUE, _DCF_CHK_SQL_INJECTION,
_DCF_INTEGER, _DCF_URL
などです。しかし、現在チェック処理が実装されているのは以下の形式のみです。
_DCF_NOT_NULL: 半角 or 全角空白 チェック_DCF_CHK_SQL_INJECTION: SQL インジェクション文字列チェック_DCF_URL: 有効な URI チェック_DCF_MAIL: 有効なメールアドレスチェック_DCF_NUMBER: 数字チェック_DCF_INTEGER: 整数値チェック脇道にそれましたが、とにかくここまでで何とか、動かすことはできる
ようになりました。
しかし「動く」だけです。
「グループ」部分は本来、sample_subテーブルのgidですので、
できればそこからグループ名を文字列で引っ張って来て表示して欲しいところ
です。INSERT時にはselectフォームでプルダウンで選択できて欲
しいです。
「管理ID」も、INSERT時にいちいち重複しないような値を覚えておくのは
苦痛です。できればデフォルトで +1 されたIDを入力済みにしておいて
くれると楽です。
いろいろと出力のカスタマイズがしたくなりますし、する必要があります。
フォームを利用したりすると、やはり見栄えにも凝りたくなります。
というわけで、見栄えをカスタマイズして行くことにしましょう。
言語に拘らず、ある程度完成したフレームワークにはプログラマーがライブラリ
本体には触らずにカスタマイズできる仕掛けが備わっています。
「フック(HOOK)」と呼ばれるときもあれば、「アドバイス(advice)」と
呼ばれるときもあります。
poko2ライブラリでは「トラッピング(Trapping)」或は
「トラッピングハンドラ(Trapping Handler)」と呼びます。
それらはフィールドの値が表示されるタイミングで呼び出され、表示対象の
フィールド名と、そのレコードにおける値が渡されてくるクラスメソッドです。
プログラマーはそれを適宜オーバーライドし、フィールド値でswtich
し、カスタマイズします。
トラッピングハンドラは大体同じ構造をしています。以下に、実際にCDbManagerで定義されているトラッピングハンドラの宣言を載せます。
// INSERT | UPDATE 時のデータ入力用フォーム出力用
function DataInputFormTrap($array, $data, $mode, &$form)
// LIST | DETAIL | INSERT_CONFIRM | UPDATE_CONFIRM時のデータ整形出力用
function GetFieldHtmlOutput($array, $data, $mode, &$html)
// INSERT | UPDATE 時のhiddenフォーム出力用
function GetHiddenFormOutput($array, $data, $mode, &$form)
// INSERT | UPDATE 用SQLの実行直前の内容調整用
function PreExecuteSqlTrap($array, $data, &$result, $mode)
// INSERT | UPDATE | DELETE 後のアクション発動用
function PostExecuteSqlTrap($mode)
以下に共通して現れる各引数を説明します。
フィールド情報を格納した配列が渡されて来ます。
フィールド名("id"などのテーブルの フィールド文字列)
フィールド名エイリアス("管理ID"など)
汎用の「サイズ」変数。inputタグのmaxlength
属性などに反映される。
データチェックフラグ、データ表示フラグ
実際にDBから取り出された対象フィールドの値が渡されます。
次の場合にカスタマイズしたHTML(入力用フォーム タグを含む)を格納する参照変数です。
モードを指示する文字列が渡されて来ます。トラッピングハンドラ
は大抵、幾つかの動作モード(list, detail, insert, ...)で使いまわされます。
そのとき、今どの動作モードでトラッピングハンドラに入って来たのかを識別
するための変数が$modeとなります。
これは少々特殊です。このタイプは参照型で、trueやfalse
ばかりでなく文字列や数値、さらには配列までを返すことが有ります。この
使い途を理解するにはpoko2のソースコードを参照する必要があるでしょう。
一般に、ユーザーがカスタマイズすべきトラッピングハンドラは次の二つです。
function GetFieldHtmlOutput($array, $data, $mode, &$html)
function DataInputFormTrap($array, $data, $mode, &$form)
GetList(), GetDetail()でDBから取得した
フィールドの値を表示する際に使われます。$mode変数には以下の文字列が
渡されて来ます。list, detail, insert_confirm, update_confirm
実際にはこれらを区別することはほとんど無いでしょう。経験上、全て同じ処理で
望みの表示形式を実現できます。具体的な実装では$array[field]でswitch
し、適切な表示形式に$dataを整形し、&$htmlに格納し、true
を返します。デフォルトで良いならfalseを返します。
InsertTable(), UpdateTable()等で入力フォーム
の出力に使います。$mode変数には"insert"又は
"update"が入ります。INSERT時は新規入力なので$data
は空です。UPDATE時は現在のDB値が入って来ます。$mode変数で場合分け
し、適切な入力フォーム用HTML出力を&$formに格納します。カスタマイズ
した場合はtrueを返し、組み込み済みのデフォルトフォーム出力処理に
任せて良いならfalseを返します。
では実際にカスタマイズしてみましょう。
最初にレコード一覧表示で「グループ」をsample_subから引っ張り、
グループ名を表示するようにしてみます。
...
class MyDbManager extends CDbManager {
...
// GetFieldHtmlOutput()をオーバーライドします。
function GetFieldHtmlOutput($array, $data, $mode, &$html)
{
switch($array[field]) {
case "gid":
// sample_sub テーブルから、gidが$dataと一致するレコード
// のgnameフィールドを取得します。
$sql = "select gname from sample_sub where gid = '$data'";
// SQL文を実行します。「SqlExec」ではなく「_SqlExec」を
// 使います。
$result = $this->SqlDriver->_SqlExec($sql);
// この分岐は 一致レコードが無かった場合の考慮です。
if($result)
$gname = $this->SqlDriver->SqlResult(0, 0, $result);
else $gname = "unknown";
// SQL結果IDを解放します。
$this->SqlDriver->SqlFreeResult($result);
// カスタムHTML出力はグループ名になります。
$html = $gname;
return true;
default:
return false;
}
}
...
};
...
実際にレコード一覧表示でグループ名が表示されるか確認します。では続いてINSERT | UPDATE時の入力フォームのカスタマイズを行います。
DataInputFormTrapをオーバーライドします。
...
class MyDbManager extends CDbManager {
...
function DataInputFormTrap($array, $data, $mode, &$form)
{
switch($array[field]) {
case "id":
// INSERT時、プライマリキーは「レコード数」+1を自動的に出力。
if($mode == "insert") $data = $this->SqlMaxId() + 1;
$form = "$data".$this->ObjForm->_HiddenForm(
array($array[field] => $data));
return true;
case "comment":
// コメント入力欄はtextareaフォームにします。
if($mode == "insert") $data = "";
$form = $this->ObjForm->_TextAreaForm(
array("rows" => 3, "cols" => 20,
"name" => $array[field],
"class" => "pdbm"),
$data);
return true;
// グループはプルダウンメニューにします。
case "gid":
// INSERT時は先頭のアイテムを選択しておきます。
if($mode == "insert") $data = 0;
$options = array();
$default = 0;
$sql = "select gid, gname from sample_sub";
$result = $this->SqlDriver->_SqlExec($sql);
for($i = 0;
$i < $this->SqlDriver->SqlNumRows($result);
$i++)
{
$gid = $this->SqlDriver->SqlResult($i, 0, $result);
$gname = $this->SqlDriver->SqlResult($i, 1, $result);
if($gid == $data) $default = $i;
$options[$gname] = $gid;
}
$this->SqlDriver->SqlFreeResult($result);
// <select><options>タグを出力します。
$form = $this->ObjForm->_SelectForm(
array("name" => $array[field], "class" => "pdbm"),
$options,
$default);
return true;
default:
return false;
}
}
...
};
...
ここまでくればだいぶ形が整って来ました。
トラッピングハンドラを実装する上で重要な注意点が有ります。それは、
SQL文を実行するためのCSqlDriverクラスメソッドです。
必ず「_SqlExec」を使い、結果IDを取得。SqlFreeResult の引数に渡して解放
してください。これをせずに普通の「SqlExec」を使ってしまうと、レコードを
取得している呼び出し元のSQL結果IDを上書きしてしまい、ぐちゃぐちゃになります。
(例えばGetListTrap()中で試してみると良いでしょう。)
poko2のソースコード自体はそれほど大規模ではないので、気になる人はSQL
ドライバファイルを読んでみると良いでしょう。デフォルト引数を併用した、関数
設計の一例が実装されています。
今回取り上げた外部テーブルから一覧を取得し、プルダウンメニューとして出力
する処理は、poko2を用いたWebアプリケーションを作る上で非常に使用頻度
が高い処理です。ぜひ中で何をしているのかを理解し、マスターして下さい。
また、poko2の提供する高水準クラス(CPoko, CPoko2)でもこれら出力
形式の整形用トラッピングハンドラだけは必ず実装する必要が有ります。慣れれ
ば定型的な、しかしアプリケーション毎に毎回異なる作業です。
以上が基本的な入力フォームなどの機能的な外見のカスタマイズです。続いて、 色やフォントなど非機能的なデザインのカスタマイズを見て行きましょう。
CSSによるカスタマイズ
純粋に文字色や背景色、入力フォームの見栄えのカスタマイズを行うには、
poko2の場合、CSSを使います。
特にCDbManagerの場合、以下のタグで必ずclassを指定しています。
"pdbm"(poko2 db manager)です。
tag.class {
...;
...;
}
形式で指示します。
また、フィールドの「エイリアス」と「値」を表示するためのtdタグでは
クラス名をそれぞれ"pdbm_f"と"pdbm_v"
に分けています。
下に今回のサンプルで使用した一例を示します。
...
<head><title>CDbManager Test Tool</title>
<style type="text/css">
<!--
a.pdbm { text-decoration: none }
a.pdbm:link { color: black; background-color: white }
a.pdbm:visited { color: gray; background-color: white }
a.pdbm:active { color: black; background-color: gray }
a.pdbm:hover { color: white; background-color: gray }
table.pdbm {
border-width: 1px;
border-style: solid;
border-color: black;
background: #ccc;
}
td.pdbm_f {
padding: 2px;
background: #ddd;
text-align: right;
}
td.pdbm_v {
padding: 2px;
background: white;
}
input.pdbm {
margin: 2px;
padding: 2px;
color: black; background: white;
border-left:1px;
border-right:1px;
border-top:1px;
border-bottom:1px solid black;
}
input.pdbmbtn {
margin: 1px;
padding: 2px;
color: black; background: #e8e8e8;
border-width: 1px;
border-style: solid;
border-color: black;
}
textarea.pdbm {
margin: 2px;
padding: 2px;
color: black; background: white;
border-width: 1px;
border-style: solid;
border-color: black;
}
select.pdbm {
margin: 2px;
padding: 2px;
color: black; background: white;
border-left:1px;
border-right:1px;
border-top:1px;
border-bottom:1px solid black;
}
span.pdbm_error {
margin: 2px;
color: red;
border-bottom: 1px dashed red;
}
strong.pdbm_error {
color: blue;
background-color: #dddde7;
border: 1px solid blue
}
-->
</style>
</head>
...
アプリケーションによっては、CSSではサポート仕切れないようなカスタマイズ
を要求されるかも知れません。CDbManagerから派生させるのではなく、CDbManagerに類する
クラスをドライバクラスから直接派生させてしまった方が速いかも知れません。CDbManagerから始める
のではなく、ドライバクラスに直接アクセスしてテンプレートと結合させる
テストツールを作成し、そこからCDbManagerやpoko2フレームワークへ
集約できるクラスにまとめあげた方が速いでしょう。
経験的に、そうした極限までカスタマイズするツールの場合、CDbManager
のようにINSERT, UPDATE, DETAILの全ての実装が要求されることはまれです。
例えばレコード一覧表示のみ、という場合もあります。そうした場合CDbManager
から始めるのではなく、ドライバクラスであるCSqlDriverから始めた方が
良いです。現実にそのような実装で作成したアプリケーションも存在します。
逆に言えばそもそもCDbManagerから始まるpoko2ユーティリティは、
そうした極端なカスタマイズをしなくても良いような、管理系裏方ツール
の作成に適しているということです。
最後にWebDBアプリケーションを作成する上で重要な「値のチェック」のpoko2
における実装と、カスタマイズ方法のサンプルを示します。
また、「フレームワークの準備」で説明をしなかったCDbManager::
IsDCEMessageBufferingメンバについても本セクションの最後で解説します。
CDbManagerで値チェックが行われるタイミングは
InsertConfirm() の先頭UpdateConfirm() の先頭dcf要素を1ビットずつ調べ、
_DCF_ビットとヒットしたら、それに対応するデータチェックハンドラを
呼び出します。falseを返し、エラーがあればメッセージを表示するなりした
後、true を返します。
_DCF_ビットとチェックハンドラの対応関係は、CDbManager::DataCheckMap
という配列中に、
CDbManager::DataCheckMap[_DCF_***] = "_DCH_function";
という形式でチェックハンドラのメソッド名として設定されます。CDbManagerのコンストラクタで最後に呼び出される
InitDataCheckMap()中で設定されます。設定される対応関係は基本的なもの
だけなので、もし自分で追加したエラーチェックビットとハンドラを登録したい
場合は、InitDataCheckMap()中の最後に呼び出される
InitDataCheckMapTrap() というトラッピングハンドラをオーバーライド
し、その中で手動で設定します。
今回はサンプルと言うことで、
_DCF_SAMPLEという新しいチェックビットを追加する。"comment"フィールドに対してこのチェックビットを
設定する。というわけで、最初に新しいデータチェックビットである_DCF_SAMPLE
を追加します。
...
require($include_dir."/kernel/cls_dbmanager.php");
define("_DCF_SAMPLE", _DCF_URL << 1);
$SqlConfig = array(
...
cls_dbmanager.phpをrequireした直後あたりで早々とdefine
します。_DCF_URLは、デフォルトで最も値が大きいビットフラグ
(0x1 << 10) です。_DCF_SAMPLEでは_DCF_URLを更にもう
1ビットシフトさせた値にします。
続いて"comment"フィールドの[dcf]に対してこのチェック
ビットを設定します。
...
array("type" => "text",
"size" => "",
"field" => "comment",
"alias" => "コメント",
"dcf" => _DCF_SAMPLE,
"dvf" => _DVF_LIST_HIDDEN),
...
何も設定しなかった初期状態では、"dcf" => 0 となっていました。
今回は新しく追加したチェックタイプである_DCF_SAMPLEを設定します。
ではいよいよ_DCF_SAMPLEに対応するチェックハンドラを作成します。
クラスメソッドとして派生先のクラスに実装します。メソッド名は"
_DCH_Sample"としましょう。
...
class MyDbManager extends CDbManager {
...
function _DCH_Sample($structure, $data)
{
$this->DataCheckError("サンプルエラー(スルー版)", $structure[alias]);
return false;
}
...
今回は「普通でない」処理をさせてみました。CDbManager::DataCheckError()は本来、第一引数に渡されたエラーメッセージ
を、赤文字センタリングで第二引数のフィールドエイリアスと一緒に表示します。
CDbManager::dceTemplateField
CDbManager::dceTemplateBody
最初にdceTemplateFieldでフィールドエイリアスの表示dceTemplateBodyでアライメントやフォントを含めた最終フォーマットcls_dbmanager.phpを参照して下さい。
話を戻しますが、通常のエラーチェックハンドラでCDbManager::
DataCheckError()を使用する場合、大概は「エラーがあったのでメッセージを
表示し、trueを返す」処理が殆どです。
cls_dbmanager.phpで既に組み込まれているエラーチェックハンドラは
おおよそ次のようなパターンになっています。
function _DCH_NotNull($structure, $data)
{
// おもに組み込み is_*** 系や、正規表現でチェックします。
if($data=="" || ereg("^( | )*$",$data)) {
// エラーが発生した場合、表示して...
$this->DataCheckError(
"$structure[alias]が書き込まれていません",
$structure[alias]
);
// true を返します。
return true;
// エラー無しなら false を返します。
} else return false;
}
つまり、DataCheckError()を使う流れでは大概trueを返すわけです。falseを返すため、「エラー無し」として扱われます。false
を返す事によりそのままスルーさせた形式にしてみました。
では最後に、この新しいチェックハンドラを登録してみます。
つまりInsertConfirmやUpdateConfir時に呼び出せるようにします。
そのためには、次の二つのクラスメソッドを派生先でオーバーライドする必要が
あります。
GetDcfEndBitについて説明します。これはデータチェックフラグの検査終了
ビットを返します。(検査開始ビットを返すメソッドとしてGetDcfStartBit()
もありますが、こちらをカスタマイズする場面はまず無いでしょう)"dcf"は、GetDcfStartBit()の返すビット
から、GetDcfEndBit()の返すビットまでの範囲で走査されます。GetDcfEndBit()は_DCF_URLを返すようになっています
ので、_DCF_URLを1ビットシフトさせた_DCF_SAMPLEはデフォルトでは
フラグ走査範囲外になってしまいます。GetDcfEndBit()を派生先でオーバーライドする必要がある
わけです。そしてCDbManager::DataCheckMap配列に チェックハンドラの
メソッド名を代入します。
...
class MyDbManager extends CDbManager {
...
function GetDcfEndBit()
{
return _DCF_SAMPLE;
}
function InitDataCheckMapTrap()
{
$this->DataCheckMap[_DCF_SAMPLE] = "_DCH_Sample";
}
...
InitDataCheckMapTrap()に関しては見たままになります。
実行してみると、「コメント」欄にどういうデータが入力されようとかならず
センタリングでメッセージが表示されるはずです。
これだけではありませんが、CDbManagerが提供しているトラッピング
ハンドラでは意図的に通常とは逆の結果を返すことにより、興味深い動作を
させることができてしまったりするものもあります。
いろいろ試してみて下さい。
最後にCDbManager::IsDCEMessageBufferingメンバについて解説します。
このメンバがtrue(1)の場合、DataCheckError()で生成されるエラー
メッセージは画面には出力されず、グローバル変数である$DCEMessageBuffer
に蓄積されていきます。
false(0)の場合、DataCheckError()が呼び出された時点で直ちに
エラーメッセージを画面に出力します。
CDbManagerのデフォルトはtrue(1)になっているので、エラーメッセージ
を出力するには明示的にDCEMessageBufferをprintするなりする必要が
あります。
今回のサンプルでは、明示的な print を省略するためfalse(0)を設定しま
した。
実際にデフォルトのtrue(1)が役に立つのはテンプレートと緻密に結合させる
場面です。データチェックエラーでエラーが発生すると、InsertConfirm()
とUpdateConfirm()はfalseを返すようになっています。その場合にのみ、
エラー用のテンプレートセクションを$DCEMessageBufferでパースして出力
する、といった用途を想像しています。
global $DCEMessageBuffer;
if(!$ObjDbManager->InsertConfirm())
print $ObjTemplate->GetBlockSectionParsed(array(
"error_body" => $DCEMessageBuffer
));
逆にエラーメッセージの表示タイミングの制御がそれほどシビアに要求されない
場合は、派生クラスで今回のサンプルのように、falseで上書きすることにより
エラーメッセージの出力コードを記述する必要がなくなります。
「CSSによるカスタマイズ」ではspanタグとstrongタグでそれぞれ
pdbm_errorクラスでスタイルシートを定義しています。
先程は触れませんでしたが、エラーメッセージの見栄えをカスタマイズするために
は上記二つのタグで、pdbm_errorクラスのスタイルシートを指定します。
spanタグとstrongタグは、CDbManager::dceTemplateBodyメンバで
次のように使われています。
var $dceTemplateBody =
"<span class=\"pdbm_error\">Input Error %s : <strong class=\"pdbm_error\">
%s
</strong></span><br>\n";
したがって、もしエラーメッセージの見栄えをカスタマイズしたい場合はサンプル
のように
span.pdbm_error {
...;
}
strong.pdbm_error {
...;
}
というように指定します。