アプリケーションプログラムの制作でどこに多くの時間を費やすか、と言ったら、ユーザインタフェースである、と答える人は自分も含めて多いと思う。 スタンドアロンからWebアプリケーションになってもそれは変わらず、リッチクライアントとか呼ばれて、「なかなか楽にならんなぁ」と思う限りだ。それで も、CakePHPを利用することで開発スピードは劇的に上がっている、と感じている。
さて、データを投入する際に少しでも利用者の負担を減らすため、セレクト要素の選択機能は極めてよく使う。そうすると次に、グループを選択することでグループに属する集合を変更したくなる。
ここでは表題通りで、グループを会社の部署やプロジェクトチームとし、グループを選択した後、そのグループに所属するユーザを選択する際に、Ajaxを利用してユーザの集合を入替える機能を実現する。
グループテーブルを「groups」とし、ユーザテーブルを「users」とする。グループとユーザの関係は多対多(hasAndBelongsToMany)として、「groups_users」テーブルを用意する。
CREATE TABLE `groups` ( `id` int(11) NOT NULL auto_increment, `name` varchar(255) default '', `created` datetime default NULL, `modified` datetime default NULL, PRIMARY KEY (`id`) ) CREATE TABLE `users` ( `id` int(11) NOT NULL auto_increment, `fdel` tinyint(2) default '0', `name` varchar(255) NOT NULL default '', `created` datetime default NULL, `modified` datetime default NULL, PRIMARY KEY (`id`) ) CREATE TABLE `groups_users` ( `id` int(11) NOT NULL auto_increment, `group_id` int(11) default '0', `user_id` int(11) default '0', PRIMARY KEY (`id`), UNIQUE KEY `group_user_key` (`group_id`,`user_id`) )
ユーザテーブルの「fdel」は、ユーザ削除で直接データを削除せず、この値を「-1」にセットすることとする。
モデルやコントローラ、ビューの作成は「bake」を利用する。「bake」を実行する際、カレントディレクトリを「APP_DIR」で行うが、 「cake.bat」コマンドにパスを通しておく必要がある。パスの通し方は、カレントを「ROOT\cake\console」にした後、
>set path=%path%;%cd%
と実行すればよい(sdozonoさんのページ)。なお、Windows上でのコマンド実行である。
実行手順を考えて見よう。そして、実行を実現する機能を拾い出す。
セレクトボックスのデータとフォームデータはbakeが作ってくれるが、コントローラのソースを見ると
$groups = $this->Group->find('list'); $this->set(compact('groups'));
として、リストデータを検索し、ビューに渡している。ビューでは、
<?php echo $form->input('User.group_id', array('options'=>$groups)); ?>
とすれば、
<div><label for="UserGroupId">Group</label> <select name="data[User][group_id]" id="UserGroupId"> <option value="1">C++</option> <option value="2">C#</option> <option value="3">BASIC</option> </select></div>
のように、HTMLを吐き出してくれる。’User.group_id’は、便宜上セットした。
CakePHPは、慣れていないと分かりにくい機能がいくつもある。ここでは、セレクトボックスに利用するデータ検索だ。モデルクラスのメソッド 「find(‘list’)」を利用すると、検索結果のデータ配列として、「array(id=>name,…)」を戻してくる。
jQueryを利用すると、わずか数行で機能を実現できる。日本でも書籍が出版されたようなので、JavaScriptも身近になると思う。
さて、jQueryを利用するためライブラリを読込むのだが、多くの人は<body>より<head>部分で読み込んだ り、プログラムを書き込んだりすると思う。ところでCakePHPを利用する場合、layoutビューに<head>部分を納めて共通で利用 することが多い。この<head>部分に動的にJavaScriptを読み込んだり、JavaScriptプログラムを書き込んだりするには どうするか?ここでは、ビュー(*.ctp)の中からそれらを行う方法を示す。
1.準備
layoutビューの<head>部分に
<?php echo $scripts_for_layout; ?>
を含めておく。コントローラでは、「Javascript」ヘルパーを追加する。
var $helpers = array('Html', 'Form', 'Javascript');
2.ビュー(*.ctp)でjQueryの読込みとプログラムを書き込む
jQueryのパックされたライブラリを、サイトに「/js/jquery.js」としてアップロードしておき、セレクトボックスのあるビューの先頭に
<?php $javascript->link('jquery', false); ?>
と指定する。
次に、グループのセレクトボックス(id=”UserGroupId”)から選択されたイベントを拾って、グループのid値を取り出し、HTTP GETメソッドのAjaxで「/users/ulserlist」へid値を渡す。受信したユーザリストでユーザ選択のセレクトボックス (id=”PrjUserId”)を入替える。
<?php $this->addScript($javascript->codeBlock( <<<JSPROG $(document).ready(function() { $("#UserGroupId").change(function() { $.get("/users/userlist/"+$(this).val(), function(data) { $("#PrjUserId").html(data); }); }); }); JSPROG )) ?>
何と、プログラム実質3行!。「あっと驚くタメゴロー」(古)だ(^_^;)。
jQueryの読込みもプログラムも、<head>部の「$scripts_for_layout」へ出力されるので確認してみて下さい。
リクエストヘッダーのAcceptは、上述の利用では「*/*」であるが、jQuery.get()の4番目のパラメータを「’xml’」などと指定することで変更できた。
サーバサイド側のプログラムで気を付ける点として、Ajax以外を受け付けるかどうするか?であろう。ここでは、ブラウザから普通に要求された場合は、404エラーを出力することにする。
アクションでAjaxによるリクエストか確認するため、コントローラに「RequestHandler」コンポーネントを指定しておく。
var $components = array('RequestHandler');
アクションプログラムで、Ajax通信の判定処理をする。
function userlist($gid=0) { if (!$this->RequestHandler->isAjax()) { $this->cakeError('error404'); }
「groups_users」から指定のグループIDを持つユーザを検索し、idとnameを取得する。この部分の検索方法は、いくつか試してみた ので次の節で述べたい。ここでは以下の方法で行う。検索条件として、ユーザテーブルの「fdel」値が「-1」のデータは、削除扱いとして検索結果から除 外する。
$this->User->recursive = 0; $users = $this->User->find('all', array( 'conditions' => array( 'fdel >=' => 0, 'id' => $this->User->GroupsUser->find('list', array( 'conditions' => array('group_id' => intval($gid)), 'fields' => 'user_id'))), 'fields' => array('User.id', 'User.name'))); Configure::write('debug', 0); $this->layout = 'ajax'; $this->set('users', $users);
ビューに渡す前に、デバッグ機能によるメッセージ出力とlayoutビューの出力を抑止しておく。次のビュー(userlist.ctp)では、ユーザリストを<option>の固まりで出力する。
<?php foreach($users as $usr) : ?> <option value="<?php echo $usr['User']['id'] . "\">" . $usr['User']['name'];?></option> <?php endforeach; ?>
ところで、出力時のレスポンスヘッダーのContent-Typeであるが、この例では「text/html」になっている。 RequestHandlerのいくつかのメソッドを実行し、その他のタイプを試みたが特に変更できなかった(Firebug使用)。何か段取りがあると 思うので、機会があれば調べてみたいと思う。なお、RequestHandlerのメソッドを利用すると、layoutをajaxにセットしなくても自動 的に変更してくれるため、同じ結果を得ることができた。
RDBMSやSQLについての浅薄さを曝け出すようで恥ずかしいのだが、もしかするとネ申からの啓示があるかも、と思ったのでちょっとだけ。
指定したグループのユーザを検索する際、例えばSQLで副照会(副問合わせ)を使って、
$qstr = "SELECT User.id,User.name FROM users AS User WHERE User.id IN " . "(SELECT GroupsUser.user_id FROM groups_users AS GroupsUser " . "WHERE GroupsUser.group_id=" . intval($gid) . ") AND User.fdel>=0;"; $users = $this->User->query($qstr);
と実行すると、1回のクエリ-で希望の結果が得られる。あるいは、
$qstr = "SELECT User.id,User.name FROM users AS User " . "JOIN groups_users AS GroupsUser ON (User.id=GroupsUser.user_id) " . "WHERE GroupsUser.group_id=3 AND User.fdel>=0;"; $users = $this->User->query($qstr);
でも1回のクエリ-である。実は、最初に検索したとき次のようにした。
$this->User->GroupsUser->unbindModel(array('belongsTo'=>array('Group'))); $users = $this->User->GroupsUser->find('all', array( 'conditions'=>array('group_id'=>intval($gid)), 'fields'=>array('User.id', 'User.name')));
ただし、groups_usersのモデル定義で「belongsTo」の「User」テーブルとの関係条件として、「conditions」に 「array(‘fdel >=’, 0)」と指定している。このため、「fdel=-1」のデータを空データとして検索結果に含めてしまう。
ビューで空データをはじき出すようにすれば、この方法も1回のクエリ-で検索結果を得られる。
前節の方法ではクエリ-を2回実行する。今回は、CakePHPで「こんな書き方もあるのかな?」と言う意味で使ってみた。もう少し考える余地がありそうなので、その時は追記するようにする。