Home XML/FrameMaker FileMaker SI会社様へ 事例ご紹介 企業情報 お問い合わせ Twitter
【Webシステム事例】
PHP版MVCフレームワークとAjaxの実装例
◎背景

弊社でシステムデザインを行った 「札幌中央へら鮒釣研究会 Information Search System」 は、札幌中央へら鮒釣研究会という釣り愛好団体の月例会成績データを管理・公開する目的で構築されたシステムです。
その背景には、以下の3つの経緯がありました。

  • 弊社社員がこの団体の成績データ管理を担当しており、そのシステム化を模索していたこと。
  • 弊社のJavaServletフレームワークのPHPへの移植が望まれており、そのプロトタイプ実装が必要だったこと。
  • 当時、はやりだしていたAjaxについて、実験を行う必要性があったこと。

システムの機能やデザインは実際のサイトをご覧いただくとして、ここではこのシステムの技術的側面、特にPHP版MVCフレームワークとAjaxの実装について解説します。

↑ ページ先頭へ

◎PHP版MVCフレームワーク

弊社のJavaServletフレームワークは、

  • Model(処理ロジック)にJavaBeans
  • View(画面表示)にJSP
  • Controller(制御)にServlet

という構成を取っています。PHPでの実装にあたっては、

  • Model(処理ロジック)にJavaBeansを模したBeansクラス
  • View(画面表示)にSmartyテンプレート
  • Controller(制御)にServletを模したControllerクラス

という構成にしました。

↑ ページ先頭へ

フレームワークを構成する基本クラス

フレームワークのベースとなるクラスは以下の通りです。

ApfController
JavaServletを模したControllerクラス。
HTTPリクエストを受け取ってからレスポンスを返すまでの一連の処理を制御します。
フレームワークを使用するアプリケーションは、このクラスを継承して必要なカスタマイズを行い、独自のコントローラクラスを実装します。
ApfBeans
JavaBeansを模したBeansクラス。
リクエストパラメタの受け渡しが主な目的となります。
フレームワークを使用するアプリケーションは、必要に応じてこのクラスを処理別に継承します。
ApfAbstractHandler
フレームワークは、リクエストに応じてコントローラが該当するハンドラに処理を振り分ける形式で動作します。
処理を振り分けるキーとなるのが、eventリクエストパラメタです。
この抽象ハンドラクラスは、eventリクエストパラメタ別に実装するハンドラクラスを抽象化したもので、Beansクラスの構築メソッド、および実際の処理メソッドをサブクラスで必ず実装するよう期待しています。
なお、フレームワークはPHP4で実装しているため、抽象クラスというものは存在しません。従って、「抽象クラスのように使用することを期待している」に過ぎません。

その他、DB管理クラス、DBアクセスクラス、ロガークラス、といった共通クラスを用意しています。

↑ ページ先頭へ

eventリクエストパラメタとハンドラおよびフォワード先のマッピング

フレームワークは、前述のようにeventリクエストパラメタに応じてコントローラが該当するハンドラに処理を振り分ける形式で動作しますが、その動作のためには、eventリクエストパラメタとコントローラが呼び出すハンドラおよびレスポンスをフォワードする先(画面)のマッピングが必要となります。
JavaServlet版フレームワークでは、通常、外部プロパティファイルでマッピングを定義しますが、PHP版では、ソースファイルの一部としてevent_map.incファイルを用意し、連想配列でマッピングを定義しました。以下にその一部を示します。

/**
 * イベントとハンドラクラスのマップ
 */
$GLB["eventmap"] = array(
    // ログイン
    "LOGIN_INIT"    =>array(    // ログイン:初期表示
                            "handler"	=>"login/LoginHandler",
                            "template"	=>"login/login.tpl",
                            "session"	=>"destroy",
    ),
    "LOGIN_SUBMIT"    =>array(  // ログイン:ログイン実行
                            "handler"	=>"login/LoginHandler",
                            "template"	=>"login/initview.tpl",
                            "session"	=>"start",
    ),
    // 例会成績
    "STAT_BYNENDO"    =>array(  // 年度別例会成績
                            "handler"	=>"search/ReikaiStatByNendoHandler",
                            "template"	=>"search/statbynendo.tpl",
                            "session"	=>"continue",
    ),
    ...
);

配列のキーとなっている"LOGIN_INIT"や"LOGIN_SUBMIT"がeventリクエストパラメタの値です。
対応する値は更に配列となっており、順にハンドラクラス名、フォワード先画面のテンプレート名、セッションの扱い方(開始・継続・破棄)の3つがマッピングされていることがわかります。

↑ ページ先頭へ

コントローラの動作

フレームワークを使用するアプリケーションでは、必ずコントローラクラスが構築され、そのメインメソッドであるservice()が呼ばれるようになっています。以下に、service()メソッドが行う処理を示します。
なお、フレームワークを使用するアプリケーションはApfControllerクラスを継承したコントローラを実装することになっているため、必要に応じて、以下の処理をカスタマイズすることが可能です。

初期処理
DB管理クラスやSmartyクラスなどの構築、初期化を行います。
eventリクエストパラメタを取得
リクエストパラメタ($_POSTや$_GET)から、eventリクエストパラメタを取得します。
セッションを初期化
eventリクエストパラメタで決定されるセッションの扱いに従って、セッションを初期化します。
イベントハンドラクラスを構築
eventリクエストパラメタで決定されるハンドラクラスを構築します。
前述のイベントマップでクラス名が指定されているため、動的に構築することができます。
イベントハンドラが使用するBeansを構築
イベントハンドラのBeansクラス構築メソッドcreateBeans()を呼び出して必要なBeansを構築します。
createBeans()は、イベントハンドラの基本クラスであるApfAbstractHandlerクラスでは具体的実装はせず(抽象メソッドのつもり)、継承先のハンドラクラスで必ず具体的実装が行われることを期待しています。
これにより、コントローラはイベントハンドラが使用するBeansクラスのことは知りませんが、期待したBeansを構築することができます。
リクエストパラメタの取得
リクエストパラメタ($_POSTや$_GET)を取得し、createBeans()で構築したBeansのプロパティに格納します。
$_FILESを確認することで、マルチパートリクエストにも対応しています。
セッション関係の処理
必要があれば、セッション内のデータをBeansに格納する、あるいはBeansのプロパティをセッションに格納する、といった処理を行います。
ハンドラの実際の処理メソッドを実行
イベントハンドラの実際の処理メソッドprocess()を呼び出して実際の処理を行います。
process()は、イベントハンドラの基本クラスであるApfAbstractHandlerクラスでは具体的実装はせず(抽象メソッドのつもり)、継承先のハンドラクラスで必ず具体的実装が行われることを期待しています。
これにより、コントローラはイベントハンドラが行う具体的処理のことは知りませんが、期待した処理を実行することができます。
eventリクエストパラメタに応じた画面にフォワード
eventリクエストパラメタで決定されるSmartyテンプレートファイルを使用して、クライアントへレスポンスを返します。
なお、ここまでの処理で致命的なエラーが発生した場合には、共通のエラー表示用テンプレートファイルが使用されます。

↑ ページ先頭へ

フレームワークを使用するアプリケーションの実装

フレームワークを使用するアプリケーションは、機能に応じたいくつかのクラスをフレームワークから継承して作成することになります。
リクエストパラメタやセッションの扱い、エラーハンドリング、テンプレートのハンドリングなどについては、フレームワークに任せることができますので、機能の実装に注力することができます。 ここでは、アプリケーションの機能としてログインおよびデータ検索がある場合の例を示します。

コントローラクラスMyAppController
前述のようにApfControllerはそのまま使用せず、必ずApfControllerを継承します。
必要に応じてApfControllerのメソッドをオーバライドします。
ログイン用BeansクラスMyAppLoginBeans
ApfBeansを継承したログイン用Beansクラスを作成し、ログイン処理に特化したプロパティを追加します。
ログイン用ハンドラクラスMyAppLoginHandler
ApfAbstractHandlerを継承したログイン用ハンドラクラスを作成します。
擬似抽象メソッドのcreateBeans()およびprocess()の2つは必ず実装します。
createBeans()では、MyAppLoginBeansの構築コードを記述します。
process()では、具体的なログイン処理コードを記述します。
データ検索用BeansクラスMyAppQueryBeans
ApfBeansを継承したデータ検索用Beansクラスを作成し、データ検索処理に特化したプロパティを追加します。
データ検索用ハンドラクラスMyAppQueryHandler
ApfAbstractHandlerを継承したデータ検索用ハンドラクラスを作成します。
擬似抽象メソッドのcreateBeans()およびprocess()の2つは必ず実装します。
createBeans()では、MyAppQueryBeansの構築コードを記述します。
process()では、具体的なデータ検索処理コードを記述します。
画面テンプレートファイル
ログイン画面、ログイン後の初期画面、データ検索指示画面、データ検索結果表示画面などのSmartyテンプレートを記述します。

↑ ページ先頭へ

◎Ajaxの採用
UIの簡略化

本システムのターゲットに想定するユーザは、比較的コンピュータ操作に不慣れな中高年層が多数を占めています。
そのため、UI設計にあたっては、直感的でシンプルな操作を実現すること、できれば「画面遷移レス」なUIを目標としていました。
本システムが提供する機能を要約すると、膨大な例会成績を年度別、例会別、釣り場別、個人別、といった条件で分類・集計し、見やすい形式にフォーマットして表示する、ということにつきます。その機能を使用するための問い合わせ指示は極めて単純で、分類キー(年度別、例会別・・・)とわずかな検索条件(年度指定、釣り場指定・・・)を指定するだけです。
その結果、以下の画面構成としました。

  • 本システムで表示する画面は1つだけ。原則として画面遷移は行わない。
  • その画面は、問い合わせ指示ナビゲータと問い合わせ結果表示エリアの2つの枠組みで構成し、ナビゲータからの指示に応じて結果表示エリアだけが更新される。
    その際、ナビゲータからの指示操作は1回だけ。1つの結果を得るために複数回の操作を行わせることはしない。

画面構成

↑ ページ先頭へ

Ajaxの採用

前述のUI設計ポリシに加えて、冒頭で述べた「Ajaxについての実験の必要性」から、ほとんどのサーバリクエストについてAjaxを採用しました。
Ajaxの実装にあたっては、フレームワークとしてprototype.jsを採用しています。

XMLHttpRequestを使ったリクエスト
本システムでサーバへリクエストを送信する際にformのsubmitを行っているケースはログイン機能ぐらいで、ほとんどのケースにおいてXMLHttpRequestを使用しています。とはいえ、サーバレスポンスをXMLで返している訳ではなく、HTMLのフラグメントを返すようにしています。
つまり、JavaScript内でXMLHttpRequestを使用してリクエストを行い、同じJavaScript内でレスポンスのHTMLフラグメントを得る。そのHTMLフラグメントで更新が必要な部分を動的に書き換える、というのが基本になっています。
「UIの簡略化」で述べた画面構成に当てはめると、問い合わせ指示ナビゲータからの指示操作でXMLHttpRequestによるリクエスト送信を行い、得たレスポンス(HTMLフラグメント)で結果表示エリアを更新する、ということになります。
Ajax.Updaterの利用
上記の基本操作を行うにあたり、prototype.jsのAjax.Updaterクラスを利用しています。
このクラスを利用すると、XMLHttpRequestを利用したリクエストに加えてレスポンスによるHTMLの更新まで行うことができます。Ajax.Updaterのコンストラクタに渡すリクエストパラメタは、From.serialize()メソッドでシリアライズして渡します。
function ajaxRequest(formId, targetId, methodType) {
    var url = $(formId).action;
    var params = Form.serialize(formId);
    var myAjax = new Ajax.Updater(
                    targetId,
                    url,
                    {
                        method: methodType,
                        parameters: params,
                        evalScripts: true
                    }
    );
}
処理中アイコンの表示
成績データ検索など比較的時間がかかる処理の指示を行う場合には、更新対象の結果表示エリアのinnerHTMLをローディング表示に変更してからリクエストを行っています。

処理中アイコンの表示

Ajaxで対応できないリクエスト
Ajax(prototype.js)で対応できないリクエストとして、マルチパートリクエストがあります。
本システムでマルチパートリクエストを使うケースは管理者機能のデータアップロードだけですが、その実装にあたっては、隠しIFrameを用意してそのIFrameをtargetとしてsubmitすることで、画面構成に影響を与えないように配慮しています。
サーバ側処理の対応
Ajaxの採用によりサーバ側で必要となる特殊な処理は、全くありません。
強いて挙げるとすれば、レスポンスを返すためのSmartyテンプレートが完全なHTMLではなくてHTMLフラグメントであることを意識する程度でしょうか。

↑ ページ先頭へ

Ajaxのメリット

Ajaxの採用、特にXMLHttpRequest利用のメリットは「画面の更新箇所を限定できる」ことにつきる、と感じます。
以下に具体的なシーンを示します。

データ更新指示の結果表示
画面の入力フォームにデータをロードし、それを編集してデータ更新を行うようなシーンでは、入力値の正当性がサーバ側でチェックされ、NGであればその旨のメッセージ表示&再入力となるでしょう。
XMLHttpRequestを利用したUIであれば、データ更新リクエストに対するレスポンスは結果メッセージだけとし、メッセージエリアだけを更新するようにできます。データ更新リクエストを送信した際のフォーム入力内容はそのまま残ります。
連動する検索条件指示
複数の検索条件があり、ある検索条件値に応じて他の検索条件の選択肢が変動するようなシーンでは、変動させる必要がある選択肢だけを更新できるXMLHttpRequestの利用は大変有効です。
検索結果リストの操作
ページング機能やソート機能が付与された検索結果リストでは、クエリストリングの一部をわずかに変更してサーバへ再リクエストすることになりますが、検索結果リストだけを更新できるXMLHttpRequestの利用は大変有効です。

↑ ページ先頭へ

◎その他
Exif情報を使った画像管理

本システムには、月例会等で撮影した画像ファイルを年度や例会指定でメタ情報付きでビューイングする機能があります。
画像ファイルとそのメタ情報の管理は、(1)両方をデータベースで管理する、(2)画像ファイルはファイルシステムで、メタ情報はデータベースで管理する、といった方法が一般的ですが、本システムでは、画像は全てデジタルカメラで撮影したJPEGファイルであることから、JPEGファイル自体に記録することができるExif情報を利用することとしました。

画像の管理場所
画像ファイルは、公開ディレクトリ上に年度別・例会別に配置しています。
画像へのメタ情報記録
デジタルカメラで撮影した新しい画像ファイルには、撮影機器情報や撮影日時などのExif情報がデジタルカメラによって記録されています。
この画像ファイルのプロパティをOSの機能で編集することにより、追加のExif情報を記録することができます。
本システムの画像ビューアでは、メタ情報のうち、撮影日時、タイトル、コメントを表示するようにしていますので、表示対象の画像はOSの機能でタイトルとコメントを追加した上で所定のディレクトリへ配置しています。
なお、トリミングやサイズ調整など画像の編集が必要になる場合でも、編集ソフトが対応していれば、Exif情報を保全することができるようです。
PHPスクリプトによるExif情報の読み出し
PHPマニュアルの「イメージ関数」に詳しい情報がありますが、まずは、--enable-exif指定でPHPをコンパイル(Windows系ならphp.iniにextension=php_exif.dll指定)してExifサポートを有効にする必要があります。
Exifサポートが有効になっていれば、exif_read_data()でExif情報を連想配列で読み出すことができます。
なお、日本語を扱う必要があるため、ini_set()でexif.encode_unicodeを空に設定しています。この設定により、読み出したExif情報をmbstringの内部エンコーディングで扱うことが可能となります。
各情報のキーはデジタルカメラ(およびプロパティを編集したOS)によって異なる可能性があるといわれていますが、いくつかのデジタルカメラおよび携帯電話で撮影した画像について、以下のキーで必要情報を取り出すことができています。
DateTimeOriginal
撮影日時(日付と時刻が空白で区切られ、日付も時刻も":"で区切られている)
ExifImageWidth
画像の幅(ピクセル)
ExifImageLength
画像の高さ(ピクセル)
Title
OSで追加したタイトル
Comments
OSで追加したコメント
画像ビューアの表示例
画像ファイルから読み出したExif情報を利用して、画像のメタ情報を表示しています。
また、<img>タグのwidthおよびheightには、それぞれExif情報のExifImageWidthとExifImageLengthを設定しています。

Exif情報を利用したスナップビューア

↑ ページ先頭へ

Google Mapsを使った釣り場マップ表示

本システムで扱う例会成績には必ず例会場所の釣り場が含まれ、成績表示や例会レポートなど随所に釣り場名が表示されますが、それらの釣り場名は全て、Google Mapsを使った釣り場マップ表示機能へのリンクに設定しています。

Google Maps API Keyの取得
Google Mapsを使用するには、事前の準備として、Google Maps APIでSign upを行い、Google Maps API Keyを取得する必要があります。
取得したGoogle Maps API Keyは、マップを表示するHTML内に下記のように指定します。
<script
 src="http://maps.google.com/maps?file=api&amp;
 v=2&amp;key=取得したキー"
 type="text/javascript">
</script>
マップの表示
釣り場マップはサブウィンドウで表示しています。
<body>のonloadで以下のJavaScript関数を呼び出し、Google Mapsオブジェクトの構築と初期設定を行っています。Google Mapsオブジェクトのコンストラクタには、マップを表示するコンテナオブジェクトを渡しています。
function mapLoad() {
    if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("mapContent"));
        // マウスホイールによる拡縮サポート
        map.enableScrollWheelZoom();
        // マップ操作コントロール追加
        map.addControl(new GLargeMapControl());
        // 地図・航空写真の切替コントロール追加
        map.addControl(new GMapTypeControl());
        // 右下のオーバービュー表示追加
        map.addControl(new GOverviewMapControl());
        map.setCenter(
            new GLatLng(43.05727501676677, 141.34086549282074),
            15
        );
    }
}
...
<div
  id="mapContent"
  style="width:626px; height:505px; margin:4px; pagging:4px;"
>
</div>
釣り場による緯度・経度の変更
釣り場マップでは、呼出元で釣り場が指定されるほか、釣り場マップ内で釣り場を変更することができます。
釣り場別の緯度・経度情報はデータベースに登録されており、サブウィンドウを開く際に全ての釣り場について緯度・経度情報を取得し、JavaScriptのコードとしてテンプレート内に埋め込んでいます。
そうすることで、釣り場マップ内で釣り場が変更された場合でも、サーバへリクエストすることなく、指定釣り場のマップへ移動させることができます。
釣り場が変更された場合のハンドラは下記のdoChangeTsuriba()で、mapオブジェクトのpanTo()で該当釣り場の緯度・経度へ移動させています。
var tsuribaInfo = Array();
tsuribaInfo[1] = new TsuribaInfo(
    "1",
    "茨戸川",
    "43.17113228474479",  // 緯度
    "141.35215759277344"  // 経度
);
tsuribaInfo[2] = new TsuribaInfo(
    "2",
    "雁里沼",
    "43.29432480345677",  // 緯度
    "141.67402267456055"  // 経度
);
...

function TsuribaInfo(id, name, latitude, longitude) {
    this.id = id;
    this.name = name;
    this.latitude = latitude;
    this.longitude = longitude;
}

function doChangeTsuriba(idx) {
    map.panTo(
        new GLatLng(tsuribaInfo[idx].latitude, tsuribaInfo[idx].longitude)
    );
}
釣り場マップサブウィンドウのリサイズ
釣り場マップサブウィンドウは比較的小さなサイズで表示していますが、ウィンドウのリサイズと連動して内部のGoogle Maps表示エリアが拡縮されるよう制御しています。
釣り場マップの表示例
指定された釣り場のマップを表示しているほか、上部のリストからの選択に応じた釣り場マップに切り替えることができます。
釣り場マップ内のコントロールを操作することで移動、拡縮、地図と航空写真の切替などの操作を行うことができます。

Google Mapsを使った釣り場マップ

将来的な構想
Google Maps APIでは、マップ上の任意の位置にマーカーを立てたり、コメント(吹き出し)を付与することができます。
この機能を利用すると、月例会の優勝者が入釣したポイントを簡単な優勝データ表示付きで示すことが可能となります。
過去の優勝データ全てに入釣ポイントの緯度・経度情報を追加する必要があるため実現には時間がかかりそうですが、入釣ポイント選定支援機能として是非実現したいと考えています。

↑ ページ先頭へ

Copyright © 2002-2016 Asahi Project, Inc. All rights reserved.

HomeXML/FrameMakerFileMakerSI会社様へ事例ご紹介企業情報お問い合わせサイトマップ

Twitter

◇当サイトのコンテンツ内に記載している企業名、製品名、ブランド名、ロゴマークなどは、一般に各社の商標または登録商標です。
◇当サイトの全てのコンテンツ(記事・情報・画像・プログラム等)の無断複写・転載を禁じます。