中級
サイト構築でFlashを使う大きな魅力の1つは、多彩な表現力です。しかし、更新頻度の高いコンテンツでは、Flashを取り入れるのは容易ではありません。Movable TypeやWordPressといった手軽に利用できるCMSが登場し、そうしたCMSとFlashを連携させるノウハウが確立されてはきたものの、プロジェクトの要件によっては合わないこともあります。
実際、弊社が最近手掛けた「新潟県弁護士会」サイトにおいて、更新頻度の高いコンテンツをFlashで作ることとなりました。いろいろと試行錯誤した結果、たどり着いたのはPHPを使ったオリジナルCMSです。本記事では、その試行錯誤の過程と、弊社オリジナルCMSの技術的なポイントを解説します。弊社の実装例はあくまでも1つの解でしかありませんが、みなさんの参考となればうれしいです。
「新潟県弁護士会」サイト
新潟県弁護士会サイトでは、当会の概要、法律相談の案内、イベントなど各種情報を提供しています。その中でも一番の特徴は、新潟県弁護士会に所属する弁護士を探せる「弁護士検索」機能です。名前、地域、取扱分野などから、希望する弁護士を探すことができます。この「弁護士検索」機能にフォーカスして解説していきます。
クライアントから「弁護士検索」機能に関する主な要望として、以下の2点がありました。
弁護士に関するサイトとなると、ただでさえ堅苦しい感じがします。そこに検索結果をテキストデータの一覧で表示させると、無機質で味気なく、利用者は弁護士をより一層遠い存在に感じてしまいそうです。そこで、サイトデザインだけでなく、検索機能でも親しみやすさを強調することをクライアントに提案しました。その提案は採用され、Flashを使って表現力豊かな検索機能を構築することになりました。
弁護士の顔写真を単純に表示するのではなく、弁護士をシンボル化した特徴のあるキャラクターデザインにし、さらにコミカルな動きつけることで、「身近な頼りになる弁護士」を演出しています。
Flashを使うことが決まったら、あとはどのようにCMSを実装するかです。開発当初、サーバ環境が未定でした。スペックの低いサーバとなった場合でも、動作・運営に支障がないように設計しなければなりません。また、サーバによってはデータベースを使用できない場合もあります。そのため、データソースをXMLとするCMSを構築することにしました。
その後、使用するサーバはPHPやMySQLを使える環境に決まったので、まず手始めにCMSとしてMovable Type 4を試してみました。クライアントは、Movable Typeの管理画面にあるブログ記事作成画面を使って弁護士情報入力を行うことになります。ただし、初期設定の入力項目では不十分なので、カスタムフィールド機能を使って、弁護士情報内容に合わせてカスタマイズしました。
いざ試験運用を行ってみると、カスタムフィールドを作りすぎたせいか(20項目以上)、動作速度が非常に遅くなっていました。また、現状のMovable Typeの管理画面UIでは、Movable Typeに不慣れなクライアントが扱いづらいため、管理画面のUIカスタマイズを試みるも、思うようにカスタマイズができませんでした。
こうした状況では、クライアントは安心して更新作業を行うことができません。そのため、CMSとしてMovable Typeを使用することは見送ることにしました。
次に検討したのが、PHPを使ってオリジナルCMSを構築することです。ゼロから作るという大変さはありますが、管理画面は分かりやすく、入力フォームもシンプルとなり、動作も快適なので、クライアント側での更新作業も無理なく行えます。以下は、実際の管理画面と入力フォームです。
このオリジナルCMSからXMLを出力します。以下は、XMLを書き出すPHPコードの例です。
$arraydata = mysql_query($sql, $db);
while ($rowdata = mysql_fetch_array($arraydata)) {
$kana_index = mb_substr(mb_convert_kana(mb_convert_kana(mb_substr(EUCJP_to_UTF8($rowdata['bar_kana']), 0, 1), "h"), "H"), 0, 1);
fwrite($fp, "¥t".'<bar id="'.sprintf("%05d", $rowdata['id']).'" sex="'.$rowdata['bar_sex'].'" icon="'.$rowdata['bar_icon'].'" kana="'.make_kana_id($kana_index).'">'.PHP_EOL);
fwrite($fp, "¥t¥t".'<name>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_name']), 'KVa')).'</name>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<kana>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_kana']), 'HVac')).'</kana>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<email>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_email']), 'rnas')).'</email>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career1>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career1']), 'KVa')).'</career1>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career2>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career2']), 'KVa')).'</career2>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career3>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career3']), 'KVa')).'</career3>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career4>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career4']), 'KVa')).'</career4>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career5>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career5']), 'KVa')).'</career5>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career6>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career6']), 'KVa')).'</career6>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<career7>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_career7']), 'KVa')).'</career7>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<comment>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['bar_comment']), 'KVa')).'</comment>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<elements id="');
$sql = "select C.id "
."from M_TRATKIBNY T inner join M_CD C on T.cd1 = C.cd1 and T.cd2 = C.cd2 "
."where T.id = '".$rowdata['id']."' "
."order by C.id";
$arraydata2 = mysql_query($sql, $db);
if (mysql_num_rows($arraydata2) > 0) {
while ($rowdata2 = mysql_fetch_array($arraydata2)) {
fwrite($fp, ','.$rowdata2['id']);
}
fwrite($fp, ',');
}
fwrite($fp, '"></elements>'.PHP_EOL);
fwrite($fp, "¥t¥t".'<office areaid="'.$rowdata['office_area'].'">'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<name>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_name']), 'KVa')).'</name>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<zip>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_postcode']), 'rnas')).'</zip>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<area>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_address1']), 'KVa')).'</area>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<address1>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_address2']), 'KVa')).'</address1>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<address2>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_address3']), 'KVa')).'</address2>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<tel>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_tel']), 'rnas')).'</tel>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<fax>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_fax']), 'rnas')).'</fax>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<hours>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_hours']), 'KVa')).'</hours>'.PHP_EOL);
fwrite($fp, "¥t¥t¥t".'<hp>'.trimFix(mb_convert_kana(EUCJP_to_UTF8($rowdata['office_url']), 'rnas')).'</hp>'.PHP_EOL);
fwrite($fp, "¥t¥t".'</office>'.PHP_EOL);
fwrite($fp, "¥t".'</bar>'.PHP_EOL);
}
fwrite($fp, '</bars>'.PHP_EOL);
fclose($fp);
そして、以下のようなXMLが出力されます。
<bars>
<bar id="00083" sex="0" icon="0" kana="0">
<name>朝妻太郎</name>
<kana>あさづまたろう</kana>
<email></email>
<career1>新潟市出身</career1>
<career2>東北大学法学部卒業</career2>
<career3>平成20年 弁護士登録</career3>
<career4></career4>
<career5></career5>
<career6></career6>
<career7></career7>
<comment>弁護士法人新潟第一法律事務所新潟事務所所属です。取扱事件等は、事務所ホームページをご覧下さい。</comment>
<elements id=",0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,55,56,57,58,59,60,61,64,65,66,67,"></elements>
<office areaid="0">
<name>弁護士法人新潟第一法律事務所</name>
<zip>950-0965</zip>
<area>新潟市中央区</area>
<address1>新光町10番地2</address1>
<address2>技術士センタービル7階</address2>
<tel>025-280-1111</tel>
<fax>025-280-1112</fax>
<hours>9:30-17:00</hours>
<hp>http://www.n-daiichi-law.gr.jp/</hp>
</office>
</bar>
...省略
</bars>
このように出力されたXMLをFlash側で解析します。解析用のメソッドは以下のようになっています。
/**
* 選択弁護士の情報設定
*/
function setText(bar:XMLList):void {
//trace(this.name + '.setText');
var id:String = bar.@id;
var tf:TextFormat;
// スクロール位置の初期化
Column1_mc.y = Column1_mc._startY;
Column2_mc.y = Column2_mc._startY;
// 写真の読み込み
BarPhoto_mc.stopLoad();
BarPhoto_mc.loadPortrait(MovieClip(this.parent).urlPrefix + 'portrait/' + id + '.jpg');
// 氏名
Column0_mc.BarName_mc.Text.text = bar.name.toString();
// ふりがな
Column0_mc.BarKana_mc.Text.text = bar.kana.toString();
// 性別
Column0_mc.BarSex_mc.Text.text=(bar.@sex=='0')?'男':'女';
// 事務所名
var barOfficeNameBox:MovieClip = Column1_mc.BarOfficeName_mc;
barOfficeNameBox.Text.width = 170;
barOfficeNameBox.Text.text = bar.office.name.toString();
// 住所
var barOfficeAddressBox:MovieClip = Column1_mc.BarOfficeAddress_mc;
var address:String = '';
if (bar.office.zip.toString() != '') {
// 郵便番号
address = '〒' + bar.office.zip.toString() + ' ';
}
if (bar.office.area.toString() != '') {
// 地域
address += bar.office.area.toString();
}
if (bar.office.address1.toString()!='') {
// 住所1
address+=bar.office.address1.toString();
}
if (bar.office.address2.toString()!='') {
// 住所2
address+=' '+bar.office.address2.toString();
}
barOfficeAddressBox.Text.width=170;
barOfficeAddressBox.Text.text=address;
barOfficeAddressBox.y=Math.floor(barOfficeNameBox.y+barOfficeNameBox.height)+SPACE_Y;
// TEL
var barOfficeTelBox:MovieClip=Column1_mc.BarOfficeTel_mc;
barOfficeTelBox.Text.text=bar.office.tel.toString();
barOfficeTelBox.y=Math.floor(barOfficeAddressBox.y+barOfficeAddressBox.height)+SPACE_Y;
// FAX
var barOfficeFaxBox:MovieClip=Column1_mc.BarOfficeFax_mc;
barOfficeFaxBox.Text.text=bar.office.fax.toString();
barOfficeFaxBox.y=Math.floor(barOfficeTelBox.y+barOfficeTelBox.height)+SPACE_Y;
// 営業時間
var barOfficeHoursBox:MovieClip=Column1_mc.BarOfficeHours_mc;
barOfficeHoursBox.Text.width=170;
barOfficeHoursBox.Text.text=bar.office.hours.toString();
barOfficeHoursBox.y=Math.floor(barOfficeFaxBox.y+barOfficeFaxBox.height)+SPACE_Y;
// メールアドレス
var barOfficeEmailBox:MovieClip=Column1_mc.BarOfficeEmail_mc;
barOfficeEmailBox.Text.text=bar.email.toString();
checkOverFlowText(barOfficeEmailBox.Text,170);
if (barOfficeEmailBox.Text.text!='') {
tf=barOfficeEmailBox.Text.getTextFormat();
tf.url='mailto:'+bar.email.toString();
tf.underline=true;
barOfficeEmailBox.Text.setTextFormat(tf);
}
barOfficeEmailBox.y=Math.floor(barOfficeHoursBox.y+barOfficeHoursBox.height)+SPACE_Y;
// ホームページ
var barOfficeHpBox:MovieClip=Column1_mc.BarOfficeHp_mc;
barOfficeHpBox.Text.text=bar.office.hp.toString();
checkOverFlowText(barOfficeHpBox.Text,170);
if (barOfficeHpBox.Text.text!='') {
tf=barOfficeHpBox.Text.getTextFormat();
tf.url=bar.office.hp.toString();
tf.target='_blank';
tf.underline=true;
barOfficeHpBox.Text.setTextFormat(tf);
}
barOfficeHpBox.y=Math.floor(barOfficeEmailBox.y+barOfficeEmailBox.height)+SPACE_Y;
// コメント
var barCommentBox:MovieClip=Column2_mc.BarComment_mc;
barCommentBox.Text.text=bar.comment.toString();
// 経歴
var barCareerBox:MovieClip=Column2_mc.BarCareer_mc;
var career:String=bar.career1.toString();
if (bar.career2.toString()!='') {
career+='¥n'+bar.career2.toString();
}
if (bar.career3.toString()!='') {
career+='¥n'+bar.career3.toString();
}
if (bar.career4.toString()!='') {
career+='¥n'+bar.career4.toString();
}
if (bar.career5.toString()!='') {
career+='¥n'+bar.career5.toString();
}
if (bar.career6.toString()!='') {
career+='¥n'+bar.career6.toString();
}
if (bar.career7.toString()!='') {
career+='¥n'+bar.career7.toString();
}
career=(career != ''?'・':'')+career.replace(/¥n*$/g,'').replace(/¥n/g,'¥n・');
barCareerBox.Text.width=205;
barCareerBox.Text.text=career;
barCareerBox.y=Math.floor(barCommentBox.y+barCommentBox.height)+SPACE_Y;
// 取扱分野
var barElementsBox:MovieClip=Column2_mc.BarElements_mc;
var e:Array=bar.elements.@id.split(',');
var elements:String='';
for (var i:uint = 1, count=e.length-1; i < count; i++) {
elements+=(elements != '' ? '¥n・' : '・')+Elements[uint(e[i])].toString();
}
barElementsBox.Text.width=205;
barElementsBox.Text.text=elements;
barElementsBox.y=Math.floor(barCareerBox.y+barCareerBox.height)+SPACE_Y;
// スクロールバー
displayScrollBar();
// 検索に戻るボタンを再開
changeEnabledButton(true);
}
今回のFlash+CMSにおける各ファイルの関係は下図のようになります。
サイト公開後、クライアントから追加の要望がありました。上述のXMLでは五十音順に弁護士データを格納してあり、検索結果は常に五十音順に弁護士が表示されるようになっていました。検索サイトの結果のように、最初の方に表示される情報ほど、利用者の目がいくものです。そこで公平な検索結果となるように、五十音順ではなく、ランダムに表示することとなりました。
しかし、ActionScript 3.0ではフィルタによってノードを取得する方法はあっても、結果をランダムに並び替えるメソッドはありません。FlashからPHPを経由して、データベース側でランダムな結果セットを取得する方法も考えられますが、追加要望を受けたときにはFlashがほぼ完成していました。そこで、PHPではそのまま五十音順のXMLを更新し、ActionScriptによるフィルタ結果をさらにランダムに並び替えるコードを書いて対処することにしました。
データソースであるXMLファイルはPHPによって五十音順に作成され、ActionScriptのXMLフィルタメソッドにより取得されるXMLListも五十音順のまま取得されます。このXMLListをそのまま使って弁護士MovieClipを表示すると、当然五十音順に表示されます。そこで、ページごとではなく、表示用XMLの弁護士をランダムに並び変える方法を採用しました。以下のようなランダム関数を利用して、五十音順に並んでいる検索結果XMLをランダムに並び変え、新しいXMLをswf内で作成するようにしています。
_xml = randomizeBar(_xml);
function randomizeBar(xml:XMLList):XMLList {
var resultXML:XMLList=new XMLList();
while (xml.length() > 0) {
var randomIndex:uint = Math.floor(xml.length() * Math.random());
resultXML += xml[randomIndex];
delete xml[randomIndex];
}
return resultXML;
}
XMLフィルタメソッドより取得された検索結果XMLから、for文を使い検索結果人数の回数だけ、ランダム関数で取得したインデックス番号の弁護士1人分のノードを取り出し、新しいXMLへ結合します。for文で回すという単純な力技で新しいXMLを作成していますが、速度的に問題がなかったのでこのロジックを採用しました。
オリジナルCMSということで、ゼロから構築するのは大変でもあります。しかし、事前にクライアントにヒアリングを行い、更新が必要な項目や事項などしっかりと洗い出し、初期の段階から出てくるであろう問題点をあらかじめつぶすことができました。また、クライアント側としても入力項目のシンプルさゆえに、迷うことなく更新作業を行うことができています。
Movable Typeなどのような既存のCMSは手軽に使うことができます。しかし、今回のプロジェクトのように、要件内容によっては動作的な問題が発生することがあり得ます。オリジナルCMSにしてしまうのも選択肢の一つではないでしょうか。