Chapter 5
さまざまなユーザーインターフェース構築
この章は、INTER-Mediator Ver.10をもとに記載しました。
このセクションでは、定義ファイルやページファイルの設定だけで可能な機能のうち、これまでに説明していない機能について説明をします。最初は一覧と詳細を行き来するユーザーインターフェース、続いて電子メールの送信、異なるクライアント間で編集結果を連動させる方法、JavaScriptで作られたユーザーインターフェース部品を利用する方法を説明します。
5-1マスター/ディテール形式のナビゲーション
一覧と詳細を切り替えて表示するようなユーザーインターフェースはよくみられます。業務系システムでは多くの場合、こうした動作が基本です。INTER-Mediatorではこうした動作を2つのコンテキストで実現して自動的に切り替えるユーザーインターフェースを用意します。そこまでの作業は、特別なプログラムコードを書かなくても、定義ファイルとページファイルの設定だけで可能です。また、一覧と詳細の切り替え時にプログラムを記述すれば、より高度なユーザーインターフェース構築も可能です。
マスター/ディテールあるいは一覧/詳細
データを一覧表示し、さらに特定のレコードについてより多くの情報を表示するといった形式のユーザーインターフェースは一般的なテクニックです。その場合、異なる画面であるだけに、2つのページを作成しておき、それぞれの機能動作を組み込むということが一般的かもしれません。しかしながら、INTER-Mediatorでは、ひとつのページに一覧表示のためのコンテキストと、詳細表示のためのコンテキストを両方を用意して、切り替えることなどが可能です。
iOSには、UISplitViewControllerというクラスがあり、iPadの「メール」などにみられるように、一覧と詳細を同時に、あるいは個別に表示できる仕組みがあります(図5-1-1)。INTER-Mediatorの機能は、このスプリットビューをヒントにしています。
FileMakerを利用するときに、レイアウトを2種類作成し、同じTOを2つのレイアウトに割り当てることで、一覧(図5-1-2)と詳細表示(図5-1-3)の切り替えがスムーズに行われます。切り替えるために、なんらかのスクリプトは必要ですが、一覧で選択した結果は、TOで記録されるので、レイアウトを切り替えるだけで、一覧で選択されているレコードを詳細レイアウトでも表示することができます。
これらのユーザーインターフェースを本コースでは、「マスター/ディテール形式」あるいは「一覧詳細形式」と総称することにして、「一覧表示側」「詳細表示側」という用語で、2種類の画面をそれぞれ特定することにします。
コンテキストに記述するnavi-controlキー
INTER-Mediatorでの一覧詳細形式のユーザーインターフェースを構築するには、原則として同一テーブルを元にした2つの異なるコンテキストを定義します。同じテーブルであっても、2つのコンテキストを定義してください。もちろんnameキーで指定する名前は別々のものにします。このとき、同一のテーブルというのは、より厳密に言えば、keyキーで指定する主キーフィールドとその値が、適切なレコードを検索する状態であるということです。極端に言えば、全く異なるテーブルでも構いませんが、主キー値を共有するものであれば、動作はします。しかしながら、それはかなり難しい運用となります。ありうる運用としては、viewキーで参照されるものが、同一のテーブルを元にしたビューであってもかまいません。それぞれが同一名の主キーフィールドを持ちkeyキーで指定されていることがポイントになります。
そして、それぞれのコンテキストでは、navi-controlキーによる値を定義します。設定可能な値は表5-1-1に記載します。この各行このnavi-controlキーの値が設定されたコンテキストは、ひとつのページファイル内で必ず2つにしてください。3つ以上ある場合の動作は保証できません。navi-controlには、そのコンテキストが一覧表示側なのか、詳細表示側なのかを指定します。詳細表示側は、recordsキーの値を「1」にしておきます。また、pagingキーは記述するとしたら、一覧表示側のコンテキストに指定をしてください。
一覧表示側 | 詳細表示側 | 動作についてのコメント |
---|---|---|
master-hide | detail | 一覧と詳細が切り替わる(detail-topと同じ) |
master-hide | detail-top | 一覧と詳細が切り替わる。一覧に戻るボタンは詳細の上部 |
master-hide | detail-bottom | 一覧と詳細が切り替わる。一覧に戻るボタンは詳細の下部 |
master-hide | detail-update | 一覧と詳細が切り替わり、詳細から一覧に戻る時に一覧が更新される |
master | detail | 一覧と詳細が同時に表示される |
2つのコンテキストの動作は、一覧表示側のnavi-controlキーの設定に依存します。「master-hide」を指定すると、前に説明したFileMakerの一覧と詳細タイプの動作をします。つまり、最初は一覧表示側だけが見えていて、詳細表示側は見えていません。一覧表示側には「詳細」ボタンが各レコードの冒頭に付加されます。それをクリックすると、一覧側は消えて、詳細表示側のみが見えます。詳細表示は1レコードだけが表示され、一覧側でクリックしたレコードが表示されます。なお「見えなくなる範囲」は原則としてエンクロージャーですが、TBODYについては、それを含むテーブル全体が見えなくなります。したがって、一番シンプルな構成は、2つのコンテキストをそれぞれ別々のTABLEタグのテーブルに表示するという手法になります。詳細表示側には、「一覧に戻る」ボタンが自動的に追加され、クリックすると、詳細表示が消えて一覧表示のみとなります。
一方、一覧表示側のnavi-controlキーの値に「master」を指定すると、前に説明した、iOSのスプリットビュー形式のユーザーインターフェースになり、一覧側、詳細側、どちらも常に表示しています。一覧表示側には「詳細」ボタンが各レコードの冒頭に付加され、クリックすると詳細側に対応するレコードが表示されます。初期状態では、詳細側は、マスター側の最初のレコードが表示されるようになっています。なお、iPadのような左右に分離された形式での表示にするには、スタイルシートの仕組みを利用して、レイアウトが意図したようになるようにします。
詳細表示側の設定値には、「-top」あるいは「-bottom」を付与することができます。この追加記述(例えば「detail-bottom」)により、そのコンテキストが詳細領域となるとともに、「一覧に戻る」ボタンをエンクロージャーの前か後かを指定することができます。また「detail」と「detail-top」は同じ意味です。
一覧表示側の設定値にも、追加記述が可能です。「detail-update」は、詳細から一覧に戻るときに、一覧のコンテキストを再表示します。データベースアクセスからやり直して、表示内容を更新します。表5-1-1には他に「-fullnavi」「-nonavi」という記述も追加できます。これらの追加記述を指定しない場合には、一覧側には「詳細」ボタンが表示され、クリックすると詳細側が表示されるように自動的になります。-nonaviを指定するとそのボタンは表示されず、自分で表示ボタンを作り込む必要があります。-fullnaviを指定すると、一覧表示側は行全体がクリック可能になり、行をクリックすることで、詳細を表示します。モバイルデバイスの場合はタッチをしても詳細を表示できるようになります。
一覧表示側に表示される「詳細」ボタン、詳細表示側に表示される「一覧に戻る」ボタンのボタン名は、いずれも、button-namesキーの配列で変更できます。それぞれのコンテキスト内で、button-namesキーの配列を定義し、要素のキーを一覧表示側は「navi-detail」、詳細表示側は「navi-back」で指定します。これらのキーの値が、ボタン名になります。
2つのコンテキストの外観をカスタマイズするには、スタイルシートを使ってさまざまに設定します。各オブジェクトに関して、表5-1-2のように、class属性を設定しています。これらのclass属性をボタン等のスタイルの変更に利用してください。なお、IM_NaviBack_TRとIM_NaviBack_TDは、詳細表示側のコンテキストのエンクロージャーがTBODYの場合だけ設定されます。このとき、THEADあるいはTFOOTに新たにTRタグ要素を作り、TDタグ要素を含み、さらにその中にBUTTON要素を配置します。これらすべてにclassを割り当てているので、うまく設定すると表の外にボタンがあるように見えるかもしれません。エンクロージャーがTBODY以外の場合、class属性がIM_NaviBack_TRとIM_NaviBack_TDの要素は作られません。
class属性の値 | 適用先 |
---|---|
IM_Button_Master | 一覧表示側に追加される「詳細」ボタンのBUTTONタグ要素 |
IM_Button_BackNavi | 詳細表示側に追加される「一覧に戻る」ボタンのBUTTONタグ要素 |
IM_NaviBack_TR | 詳細表示のエンクロージャーがTBODYの場合、「一覧に戻る」ボタンを含むTRタグ要素 |
IM_NaviBack_TD | 詳細表示のエンクロージャーがTBODYの場合、「一覧に戻る」ボタンを含むTDタグ要素 |
一覧と詳細の切り替え時に呼び出されるメソッド
一覧表示と詳細表示で、コンテキスト内のリンクノードについては、データベースの内容がそれぞれ表示されますが、それ以外のなんらかの処理を追加したい場合には、いくつかのメソッドを利用することができます。少ない作業で確認ができるので、このセクションの演習で実際にプログラムを追加して動作を紹介しておきます。
演習一覧と詳細を利用したユーザーインターフェース
iPadのようなユーザーインターフェースや、一覧と詳細が切り替わるユーザーインターフェースを実際に作成してみましょう。また、JavaScriptを利用した高度なカスタマイズも紹介します。
2つのコンテキストを定義ファイルに定義
viewとtableは「person」とします。
viewとtableは「person_layout」とします。
viewとtableは「person」とします。
viewとtableは「person_layout」とします。
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
ページファイルの作成と表示
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def11.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<tr>
<td>
<div data-im="person_list@name"></div>
<div data-im="person_list@mail"></div>
</td>
</tr>
</table>
<table>
<tr><th>id</th>
<td data-im="person_detail@id"></td>
</tr>
<tr><th>name</th>
<td><input type="text" data-im="person_detail@name"/></td>
</tr>
<tr><th>address</th>
<td><input type="text" data-im="person_detail@address"/></td>
</tr>
<tr><th>mail</th>
<td><input type="text" data-im="person_detail@mail"/></td>
</tr>
<tr><th>category</th>
<td><input type="text" data-im="person_detail@category"/></td>
</tr>
<tr><th>checking</th>
<td><input type="text" data-im="person_detail@checking"/></td>
</tr>
<tr><th>location</th>
<td><input type="text" data-im="person_detail@location"/></td>
</tr>
<tr><th>memo</th>
<td><textarea data-im="person_detail@memo"></textarea></td>
</tr>
</table>
</body>
</html>
<body>
<div id="IM_NAVIGATOR"></div>
<div style="display: flex; gap: 1em">
<table>
:
</table>
<table>
:
</table>
</div>
</body>
同一ページでのマスター/ディテール形式のユーザーインターフェース
<body>
<div id="IM_NAVIGATOR"></div>
<div style="display: flex; gap: 1em">
<table>
<tr>
<td></td>
<td>
<div data-im="person_list@name"></div>
<div data-im="person_list@mail"></div>
</td>
</tr>
</table>
一覧表示側に対して作用するページネーション
レコード削除に対する詳細側の動作
<table>
<tr>
<td>
<div data-im="person_list@name"></div>
<div data-im="person_list@mail"></div>
</td>
<td></td>
</tr>
</table>
一覧と詳細が切り替わるユーザーインターフェース
エンクロージャー外の要素のコントロール
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def11.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.naviAfterMoveToDetail
= function(master, detail){
document.getElementById("master_title").style.display = "none";
document.getElementById("detail_title").style.display = "block";
}
INTERMediatorOnPage.naviAfterMoveToMaster
= function(master, detail){
document.getElementById("master_title").style.display = "block";
document.getElementById("detail_title").style.display = "none";
}
</script>
</head>
<body>
<h1 id="master_title">住所録一覧表示</h1>
<h1 id="detail_title" style="display:none">住所録詳細表示</h1>
<div id="IM_NAVIGATOR"></div>
<div style="display: flex; gap: 1em">
<table>
<tr>
<td></td>
<td>
<div data-im="person_list@name"></div>
演習のまとめ
- コンテキスト定義のnavi-controlに「master」「detail」をそれぞれ同一のテーブルから定義した2つのコンテキストに記述することで、一覧と詳細を表示するユーザーインターフェースが作成できます。
- 一覧および詳細のコンテキストでは、その他の設定にも注意を払う必要がありますが、通常、一覧は複数レコードを表示、必要に応じてページネーションと連動させます。詳細は1レコードを表示するようにします。
- 「master-hide」を指定すると、一覧と詳細が切り替わるユーザーンタフェースを作成できます。
- 一覧から詳細、あるいは詳細から一覧に移動するときに、作成しているページ側で定義したプログラムを実行することができます。これを利用して、例えば「master-hide」を利用しているときに、一覧と詳細で異なる見出しを表示することなどが可能です。
一覧と詳細の切り替え時に呼び出されるメソッド
以下、マスター/ディテール形式のページにおいて使用可能なJavaScriptのAPIについて説明をします。JavaScriptについては、本ページよりも後になりますが、『2-3 JavaScriptプログラムの記述』の記述を踏まえたものとします。
一覧表示と詳細表示で、コンテキスト内のリンクノードについては、データベースの内容がそれぞれ表示されますが、それ以外のなんらかの処理を追加したい場合には、いくつかのメソッドを利用することができます。一覧表示から詳細表示、あるいは詳細表示から一覧表示に切り替わるとき、切り替え前に呼び出されるメソッドと、切り替わり後に呼び出されるメソッドの、合計4種類のメソッドを、グローバル変数INTERMediatorOnPageのメソッドとして定義可能です。引数を2つ取り、mContextが一覧表示側、dContextが詳細表示側のコンテキストオブジェクトです。コンテキスト定義ではなく、モデルとして動作するコンテキストオブジェクトへの参照が得られます。返り値は不要です。
INTERMediatorOnPage.naviBeforeMoveToDetail = function (mContext, dContext) {...}
一覧表示から詳細表示に切り替わる前に呼び出されます。
INTERMediatorOnPage.naviAfterMoveToDetail = function (mContext, dContext) {...}
一覧表示から詳細表示に切り替わった直後に呼び出されます。
INTERMediatorOnPage.naviBeforeMoveToMaster = function (mContext, dContext) {...}
詳細表示から一覧表示に切り替わる前に呼び出されます。
INTERMediatorOnPage.naviAfterMoveToMaster = function (mContext, dContext) {...}
詳細表示から一覧表示に切り替わった直後に呼び出されます。
これらの4つのメソッドは、一覧/詳細の切り替わるときにしかこれらのメソッドは呼び出されません。ページ表示直後に何らかのプログラムの追加が必要なら、INTERMediator.doBeforeConstructメソッドの呼び出し前や、INTERMediatorOnPage.doAfterConstructメソッド(『8-5 ブラウザーを判断するページ』を参照)を利用します。
マスター/ディテール形式のページでのそれぞれのコンテキストの取得
マスター表示とディテール表示の切り替え前後に呼び出されるメソッドは、それぞれのコンテキストオブジェクトを引数として渡されるので、コンテキストは即座に参照できます。これら以外のメソッドで、マスターあるいはディテールのコンテキストを得るには、次のAPIを利用することができます。
IMLibContextPool.getMasterContext()
マスター側のコンテキストへの参照を返します。
IMLibContextPool.getDetailContext()
ディテール側のコンテキストへの参照を返します。
詳細へのナビゲーション
IMLibPageNavigation.moveToDetail(keyField, keyValue, isHide, isHidePageNavi)
引数に指定した仕様の詳細画面を表示します。一覧側にある「詳細」ボタンを押したときに利用されるメソッドです。
引数 | 指定内容 |
---|---|
keyField | 詳細側に表示するレコードのキーフィールド名 |
keyValue | 詳細側に表示するレコードのキーフィールド値 |
isHide | 一覧側を非表示にするのならtrue、そのままならfalse |
isHidePageNavi | ページネーションを非表示にするのならtrue |
このメソッドの返り値は、関数です。その関数を引数なしで呼び出すことで、詳細への画面切り替えが発生します。例えば、マスター領域の最初のレコードに対応した詳細を表示するには、リスト5-1-1のようなプログラムで可能です。
var context = IMLibContextPool.getMasterContext();
var keys = Object.keys(context.store);
var comp = keys[0].split('=');
var func = IMLibPageNavigation.moveToDetail(comp[0], comp[1], true, true);
func();
リスト5-1-1のプログラムを利用すると、最初に一覧表示のマスター領域ではなく、最新データの詳細を表示することができます。リスト5-1-2にあるように、INTERMediatorOnPage.doAfterConstructメソッドに処理を記述します。INTERMediator.partialConstructingプロパティは、ページ全部が生成し終わっているかどうかを判定できるものです。最初のページ生成ではfalseになっていて、その後のマスターと詳細を行き来するときにはtrueになります。つまり、ページの最初の生成時にのみ、moveToDetailメソッドが実行されるということです。moveToDetailメソッドの返り値は関数なので、「func();」のように即座に実行しています。ここで、詳細側にどのレコードを表示するのかを、moveToDetailメソッドの引数に指定します。詳細側のコンテキストからレコードを取り出すとき、「第1引数="第2引数"」といった条件式が適用されると考えてください。一方、マスター領域はすでにコンテキストが作られています。マスター側のコンテキストを取得するには、IMLibContextPool.getMasterContextメソッドが手軽で便利です。そのオブジェクトのstoreプロパティがデータベースから取得したデータですが、「主キー=値」がプロパティとなったオブジェクトの形式になっています。Object.keysメソッドで、プロパティ名の配列を得て、その最初の要素から、主キーフィールド名とその値を分離して得ています。
INTERMediatorOnPage.doAfterConstruct = function () {
if (!INTERMediator.partialConstructing) {
var context = IMLibContextPool.getMasterContext();
var keys = Object.keys(context.store);
var comp = keys[0].split('=');
var func = IMLibPageNavigation.moveToDetail(comp[0], comp[1], true, true);
func();
}
}
IMLibPageNavigation.moveDetailOnceAgain()
最後に行った詳細画面への移行を再度行います。マスター/ディテール形式のページから別のページに移動し、またマスター/ディテール形式のページに戻ったときに、以前表示していた詳細レコードを再度表示したいような場合に利用できます。
このセクションのまとめ
一覧と詳細を行き来するユーザーインターフェースや、一覧と詳細を同時に表示するユーザーインターフェースを、定義ファイルへの指定だけで作成可能な機能がINTER-Mediatorには搭載されています。2つのコンテキストを用意して、navi-controlキーにmasterやdetailなどの値を指定することが基本です。レコードの削除の動作には若干、気を付ける必要がありますが、2つのコンテキストは連動しているので、一方で編集した結果はもう一方に原則として即座に反映されます。ナビゲーションのためのボタンが自動的に付けられますが、ボタン名のカスタマイズや、スタイルシートによる書式設定もできます。
5-2メールの送信
Webアプリケーションでは、メールの送信を行うことがよくあるため、INTER-Mediatorでもメール送信の機能を実装しました。単に送信するだけなら、定義ファイルへの設定のみで可能です。また、メール送信を一般化して、「データベース処理の後に、メッセージを送る」という機能に更新しており、メール以外にSlackへのメッセージ送信も可能です。他のメッセージングサービスについてもAPIがあれば、プラグイン的に対応は可能です。
メールを送るタイミング
まず、メールを送るタイミングについて説明をします。メールの送信をクライアントから実行するという手もありますが、ブラウザーからクライアントOSや別のアプリケーションを操作するのはかなり難しく、セキュリティ面から、原則として大きく制約されているのが一般的です。そのため、メールを組み込む機能はサーバー上にある必要があります。
そのこともあって、メールの送信機能は、「データベースに対する操作を行った後」に行うという実装としました。ただし、レコード削除後に送信するのは用途的に考えにくいのと、レコードの内容をメールに含める仕組みを実現しようとすると、この処理だけ例外的になってしまうので、削除は対象外としました。つまり、データの基本操作であるCRUD(Create Read Update Delete)のうちのCRUの3つの操作の後に、メールを送ります。コンテキストに対してsend-mailキー、あるいはmessagingキーで連想配列を定義し、その要素のキーとして、表5-2-1のようなキーを指定します。言い換えれば、ひとつのコンテキストについて、CRUそれぞれにメール送信やメッセージ送信の指定を指定できるようになっています。
キー | 動作 |
---|---|
driver | 送信処理に利用するドライバの名称で、省略するとメール送信、値として、"mail"あるいは"slack"をサポート。なお、send-mailキーではこの要素は記述してはいけない。 |
read | レコードの取り出しを行った後にメールを送信する |
upate | レコードの更新処理を行った後にメールを送信する |
create | 新たなレコードを作るアクションを起こした後にメールを送信する |
設定の上で若干柔軟性が低いと思われるかもしれません。例えば、フィールドAを更新したときだけメールを出したいといった場合があるとします。そのようなときには、フィールドAの更新を行うときのコンテキストを新たに定義し、そこにメール送信の設定を行います。そして、フィールドAの更新を、例えばボタンを押して行うなどして、ボタンを押したときに新たなコンテキストの更新処理をJavaScriptで記述するという手法を使います。このように、メールの送信のための別のコンテキストを用意するといった手法で、柔軟にメール送信の仕組みが組み立てられると同時に、条件設定的な複雑な設定やプログラムを導入することなくメール送信が利用できます。
createやupdateの場合は、返ってくるコンテキストの内容は1レコードのみです。そして、その1レコードに対応したメールが送信されます。すなわち、メールの内容として、そのレコードのフィールドの値を入れ込むなどが可能です。readの場合は、複数のレコードがコンテキストに含まれるかもしれません。その場合は、1レコードにつき1通のメールが送られるのが基本です。つまりレコードごとに異なるフィールドの値をメールに入れ込めば、1通1通の内容が異なるメールを送信できます。フィールドの内容を入れ込む方法は、この後の演習で具体的に説明します。
メール処理の動作に関する設定
INTER-Mediatorのメールを送る機能は、Ver.5までの仕組みと、Ver.6以降の仕組みが大きく異なっています。ここでは前者を「旧アーキテクチャ」、後者を「新アーキテクチャ」と呼びます。Ver.6を実装する段階で、旧アーキテクチャの実装は残して新アーキテクチャも組み込み、相互に切り替えて運用できるようにしました。また、過去のアプリケーションとの互換性を考慮して、Ver.6での既定値は旧アーキテクチャでの稼働を規定値にしました。しかしながら、今後は新アーキテクチャが使われることがメインになると想定して、Ver.10で既定値を新アーキテクチャとしました。旧アーキテクチャに切り替えることは可能です。新アーキテクチャのみをこのドキュメントでは紹介します。
もし、旧アーキテクチャで動作させる場合には、params.phpファイルに$sendMailCompatibilityMode変数を定義してtrueを代入してください。この変数の規定値は、Ver.6〜9ではtrue、Ver.10以降はfalseになっていますので、以前のソースを利用する場合にはparams.phpにこの変数設定がないかをチェックしてください。
メールの内容に関する設定
あるコンテキストで、新規にレコードを作ったときにメールを送るのであれば、send-mailキー(messagingキー)のcreateキーの値にさらに連想配列を定義して、その連想配列を、表5-2-2に示すキーの要素を追加します。表にあるすべてのキーを設定する必要はありませんが、送信者と送信先、そして本文の3つはなんらかのキーで指定は必要です。
キー | 値に設定する内容 |
---|---|
from | 送信者名や送信者アドレスが含まれるフィールド名 |
to | 送信先が含まれるフィールド名 |
cc | Cc先が含まれるフィールド名 |
bcc | Bcc先が含まれるフィールド名 |
subject | 件名が含まれるフィールド名 |
body | メール本文が含まれるフィールド名 |
template-context | 本文のテンプレートとなるコンテキストとレコードを指定 |
store | メール送信の記録を行うコンテキストを指定 |
attachment | メールにファイル添付を行う場合 |
f-option | UNIXでSMTPサーバーを経由しない場合にtrueを指定すると、fromの指定が有効 |
body-wrap | 右端の折り返しのバイト数(指定がないと72バイト)。0だと折り返ししない |
メールの作成方法は、この後に演習を通じていくつかの事例を示しながら解説をします。宛先や送信者、本文は、対応するキーに対する値がそのまま出力されますが、コンテキストのレコードにあるフィールドの値をそこに入れ込むには「@@@フィールド名@@@」のような記述を使います。その部分がフィールドの値に置き換わってメール送信されます。また、メールの元データをデータベースに入れておき、それを利用することも可能です。
表の中のf-optionは、UNIXマシンのsendmailコマンドを使ってメールを送るときに、送信者を指定したのにもかかわらず、送信者が、www(あるいはApacheの稼働ユーザー)になってしまうようなときに指定をしてください。OSに組み込まれているメール送信コマンド等の動作に依存しますが、より確実に送信者の指定ができるはずです。
body-wrapは、長い行の折り返しを、Shift-JISのバイト数で指定します。なお、折り返しを入れますが、比較的追い込みを積極的に行うアルゴリズムを組み込んであります。
演習Post Onlyモードのページでメールを送信する
Post Onlyモードでは、アンケートなどで使われることが多いと思われます。そのとき、投稿した上で、確認のメールを出すということはよく行われます。そうした場面を想定して、3種類のメールの送信方法を紹介します。
演習環境にメールサーバのコンテナを追加する
:
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
ports:
- 127.0.0.1:13306:3306
# mailhog:
# image: mailhog/mailhog
# ports:
# - "8025:8025"
# - "1025:1025"
docker-compose up -d
コンテキストを定義ファイルに定義
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
Post Onlyモードのページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def12.php"></script>
</head>
<body>
<table>
<tbody data-im-control="post">
<tr>
<th>お名前</th>
<td><input type="text" data-im="survey@Q1"/></td>
</tr>
<tr>
<th>メールアドレス</th>
<td><input type="text" data-im="survey@Q2"/></td>
</tr>
<tr>
<th>ご意見</th>
<td><textarea data-im="survey@Q3"></textarea></td>
</tr>
<tr>
<th></th>
<td><button data-im-control="post">送信</button></td>
</tr>
</tbody>
</table>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>お名前</th><th>メールアドレス</th><th>ご意見</th></tr>
</thead>
<tbody>
<tr>
<td data-im="survey_list@Q1"></td>
<td data-im="survey_list@Q2"></td>
<td data-im="survey_list@Q3"></td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
送信者、送信先、本文がすべて一定のメールの設定
以下の演習では、メールアドレスを使用しますが、mailhogのコンテナを使っている限りは、メールアドレスに何を指定しても、実際にはメールは送られませんし、メールを送ったということをブラウザで確認できます。筆者の新居雅行のメールアドレスを記述しますが、そのまま指定しても構いませんし、ご自分のメールアドレスを指定されても構いません。
フォームの入力をきっかけとしてメール送信される
データベースから得られた宛先を送信者にする
メール本文にテンプレートを使用する
mysql -u web -h 127.0.0.1 -P 13306 --password=password test_db
select * from mailtemplate;
演習のまとめ
- Post Onlyモードを利用してレコードを新規作成するときに、メールを送信する方法を演習を通じて学びました。
- 送信者と件名は固定でしたが、宛先や本文を固定あるいはフィールドの値を利用する方法を説明しました。
- 本文をテンプレートから抜き出してきて差し込む方法を学びました。本文をテンプレートとなるテキストファイルに記述する方法が実用的な意味で汎用性が高く、さまざまな文面を手早くシステムに組み込めて便利に使えるでしょう。
SMTPサーバーを利用してメールを送信する
INTER-Mediatorでのメール送信は、Symphony MailerというPHPのライブラリを使用しています。SMTPに関する設定を与えない場合はPHPのmail関数を使って独自にエンコードしてメール送信しますが、現状のインターネット環境ではその利用方法はほとんどあり得ないと思われます。SMTPサーバ経由あるいはGmailやAmazon SESを利用するのが一般的でしょう。
SMTPサーバーを利用するための設定は、定義ファイルのオプション部分、あるいはparams.phpファイルに設定します。オプション部分にsmtpキーの要素を定義し、その値の連想配列の要素に表5-2-3のキーと対応する値を指定することで、SMTP通信を行ってメールのリレーを別のサーバーに依頼することができます。params.phpファイルでは、$sendMailSMTP変数に、表5-2-3のキーを持つ連想配列を代入します。
キー | 対応する値 |
---|---|
protocol | メール送信のプロトコルで通常は'smtp'(Ver.11で実装) |
port | メール送信時に使用するサーバーのポート |
encryption | 暗号化のプロトコルで、'ssl'ないしは'tls'(Ver.11で廃止) |
username | メール送信時に認証で使用するユーザー名 |
password | メール送信時に認証で使用するパスワード |
定義ファイルエディターで実際に設定する場合は、ページの冒頭にある「Show All」ボタンをクリックして、全項目を表示します。すると、図5-2-1のように、Optionsの最後に設定項目が表示されるようになります。定義ファイルへの設定を直接行う場合は、リスト5-2-1にあるように、オプション領域に記述を行います。
IM_Entry(
array ( // コンテキストの定義
array (
'name' => 'survey',
:
),
),
array ( // オプション設定
'smtp' => array (
'protocol' => 'smtp',
'server' => 'mail.msyk.net',
'port' => 589,
'username' => 'msyk_test',
'password' => 'testpassword',
),
),
array ( // データベース接続設定
'db-class' => 'PDO',
:
),
false);
Symphony Mailerでは、SMTP以外のメール送信の方法にもサポートしています。このうち、GmailやAmazon SESを利用する場合の設定方法を以下に示します。Gmailの場合のpasswordは、通常のパスワードではなく、別途発行するアプリパスワードです。Amazonの場合はIAMで発行したアカウントに対して生成されるアクセスキーとシークレットキーを指定します。なおSymphony Mailerの全てのプロトコルに対応してはいないので、これら以外のプロトコルの場合は必要なライブラリ等がインストールされているかなどを確認してください。必要なら、追加でインストールをしてください。
$sendMailSMTP = array(
"protocol" => "gmail+smtp",
"username" => "msyk.nii83@gmail.com",
"password" => "himitsunoapripassword",
);
$sendMailSMTP = array(
"protocol" => "ses+https",
"username" => "yourACCESSKEY",
"password" => "yourSECRETKEY",
);
メールのテンプレートを保存しておくテーブル
メールをテンプレートから生成したい場合、テンプレートを表5-2-4のようなフィールド構成のテーブルに保存しておき、send-mailキー(messagingキー)以下のtemplate-contextキーに、そのテーブルからコンテキスト名を記述します。このコンテキストが定義されていて、読み出しが作成が可能な状況になっている必要があります。フィールド名はカスタマイズ等できないので、この名称を利用する必要があります。スキーマのサンプルでは、mailtemplateテーブルとして定義されています。フィールドに入れるべきデータについてはフィールド名を見れば明白です。
フィールド名 | 型 |
---|---|
id | INT(主キー) |
to_field | TEXT |
bcc_field | TEXT |
cc_field | TEXT |
from_field | TEXT |
subject | TEXT |
body | TEXT |
template-contextキーには、このテーブルのコンテキスト名に加えて、実際にテンプレートとして使用するレコードを指定します。ここで、コンテキストとして「mailtemplate」が定義されている場合、template-contextキーには「mailtemplate@id=1」のような記述を行います。@は決められた記号です。その後に、主キーフィールド名、イコールに続いて、該当するレコードのidフィールドの値を記述します。
メール送信結果を残す
メールの記録を残したい場合は、send-mailキー(messagingキー)以下のstoreキーに、そのコンテキスト名を記述します。このコンテキストは、表5-2-5のようなフィールドを持つテーブルであって、新規レコード作成が可能な状況になっている必要があります。フィールド名はカスタマイズ等できないので、この名称を利用する必要があります。スキーマのサンプルでは、maillogテーブルとして定義されています。フィールドにそれぞれ何が入るからはフィールド名から明白です。
フィールド名 | 型 |
---|---|
id | INT(主キー) |
to_field | TEXT |
bcc_field | TEXT |
cc_field | TEXT |
from_field | TEXT |
subject | TEXT |
body | TEXT |
errors | TEXT |
foreign_id | INT(外部キー) |
実際のテーブル定義では、レコード作成時の日付時刻が入るdtフィールドもありますが、INTER-Mediatorの基本機能で利用されるのは、図5-2-1のフィールドのみです。storeキーに指定したコンテキストに対してcreateオペレーションつまりレコード作成が行われます。ここで、relationキーを指定しておくことで、join-fieldキーのフィールドに対応する値が、foreign_idフィールドに入力されます。なお、foreign_idという名前である必要はなく、外部キーを設定するフィールドは、relationキーのforeign-keyキーの定義から取り出されます。また、storeキーに指定するコンテキストで、queryキーを指定すると、その検索条件が初期値として判定されて、それらのフィールドへの自動入力も可能になっています。
メール送信を伴う機能組み込みのパターン
このセクションの演習は、新規レコード作成とメール送信を連動させるという非常に分かりやすい事例を使いましたが、実際のアプリケーションではさまざまな状況でのメール送信のニーズが発生するでしょう。例えば、マネージャーが承認したら、関係者にメールが飛ぶといったような用途を考えてみましょう。このような場合、どのように機能を組み込めば良いのでしょう。もちろん、個別の事情によって異なると言えばその通りなのですが、いくつかの組み込みパターンがあり、それをヒントにすれば、機能の組み込み方法は見えているかもしれません。ここでは2つのパターンを紹介します。
まず、最初のパターンは「メール送信用のコンテキストを定義する」ということです。つまり、一覧を表示したり、レコードの修正を行うためのコンテキストとは独立したメール送信専用のコンテキストを作ります。何種類かメールがあれば、それぞれコンテキストを作ります。こうすれば、例えば、「承認」というのは、「メール送信用コンテキストを通じて特定のフィールド(例えば「承認日」)に現在の日付を入力する」というデータベースの操作に置き換えることができます。この操作は、JavaScriptの記述が便利なので、『6-3 データベースへの書き込みを直接行う』で具体的なサンプルを示します。こうして、特定の処理だけ、メールの送信を伴うコンテキスト上で処理を進めるということで、他の処理とメール処理が混同することはなくなります。
もうひとつのパターンは「メール送信コンテキストのviewキーの値を効果的に使う」ということです。メールの文面は、ファイルで用意したテンプレートを利用すると、長いものでも管理はしやすいでしょう。しかしながら、ここで問題になるのは、必要なフィールドの値をきちんとメールに含めることができるかどうかです。そのためには、メール作成時にどんなレコードが得られるかを知る必要があります。
新しいレコードを作成すると、その作成したレコードの主キー値を使って、viewキーで指定したテーブルやビューに対して検索をかけて、新規に作成したレコードを取り出し、そのフィールドの値をメール内の「@@フィールド名@@」の記述に置き換えることができます。このとき、新規レコードを作成したテーブルから検索をしてもいいのですが、SQL系のデータベースだとビューを定義することで、レコードを作成したテーブルにないフィールドも、関連付けを辿ってビューの結果に含めておくことで、メールに含めることができます。例えば、この演習ではメールアドレスを入力しましたが、顧客マスターのようなテーブルがあるなら、メールアドレスから名前や会社名を取り出して、それもメールに含めることも可能になります。そのためには、surveyテーブルと顧客マスターをJOINし、回答に加えてその顧客の名前や会社名、部署等をフィールドとして含むビューを作成します。こうした、メール専用のコンテキストで、メール専用のビューを作って使うということを行えば、別のテーブルの内容もメールに含めることができます。FileMakerの場合はレイアウトに、同一のTOG(テーブルオカレンスのグループ)に含まれるフィールドを配置することで、SQLのビューに近いことが可能です。
レコードの更新を行う場合、あるレコードのあるフィールドが更新されると、そのレコードの主キー値を使ってviewキーの値のテーブルあるいはビューに対して検索をかけて、得られたレコードをメールの文面に含めることができます。レコードの検索の場合は、検索結果の最初のレコードから、指定のフィールドが抜き出されます。そのため、レコードの検索を行った後、その結果情報をメール送信に含めたい場合には、検索結果が基本的にひとつに絞られるようなものでないと、正しく動作しないかもしれません。
メール送信のトラブルシューティング
メール送信のトラブルは、現実には単にキータイプミスという場合がほとんどではないかと思われますが、加えて、ネットワーク制限を認識しているかどうかという点も重要ではあります。しかしながら、トラブルに遭った方は「正しく設定しているはずなのに」という前提から抜け出せないことで、なかなか対処できないということにもなりがちです。ともかく、冷静かつ客観的に設定や状況を確認してください。「間違い」を除くと、以下のような原因が考えられます。
- そもそもサーバーにメール送信機能がない。その場合はサーバーの動作を変えるか、SMTPサーバーへ中継する。
- サーバーにメール送信機能はあるが送られない。その場合はサーバーの動作で何か設定が必要かもしれない。あるいはどこかでファイアウォールによってSMTP通信が遮断されていることが多い。
- 文字化けする。その場合は定義ファイルやテンプレートのファイルがUTF-8でエンコードされているかどうかを確認する。
- 送信者が指定通りにならない。その場合は、UNIX系OSなら、f-optionキーの値を「true」にする。
- SMTPサーバーを指定したが送られない。原因は別掲します。
SMTPやあるいは認証のトラブルはさらに複雑な設定が絡みます。うまく行かない場合には、以下のような原因が考えられます。
- サーバー名がまちがっている。
- INTER-Mediatorのサーバーと、SMTPサーバーの間で、利用するポートの通信ができるのかどうかをよく確認する。
- ポート番号が違っている。Gmailの送信機能は587です。そのほか、25なのか、587なのか、ないとは思われるが465なのか、よく確認する。
- ユーザー名、パスワードが違っている。正しいと思っている方こそ、絶対に違っています!と言い切れるほど、この手の間違いは多い。Gmailのユーザー名は、@を含むメールアドレス全てがユーザー名です。
- SMTPサーバーが送信者の制限を行っている。例えば、一般にプロバイダーは、契約しているドメインの送信者のメールしか送れません。任意のアドレスを送信者にできないのが一般的です。
- あなたの利用しているSMTPサーバーがブラックリストに入っている。未だにけっこう無意味な「怪しいホスト名リスト」にあるメールを拒否する設定をしている会社が、いくらかはあります。これは、送信できているけれども、受け入れてもらえないだけです。INTER-Mediatorやあるいは関連ライブラリの問題ではありません。
Slackのタイムラインに投稿する
メール送信の代わりに、Slackへの投稿も可能です。まず、Slackへの投稿を行うには、「chat.postMessage」というAPIへの送信が可能なトークンを発行します。そのトークンと、投稿チャンネルに関して、params.phpファイルに以下のような設定を行います。なお、IM_Entry関数の第2引数(オプション変数)に対して、slackキーに対する連想配列で、tokenとchannelキーを与えて指定しても構いません。オプション変数の方がparams.phpよりも優先されます。
$slackParameters = [
"token" => 'xoxp-XXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
"channel" => 'message-posting-test',
];
そして、コンテキスト定義側は、messagingキーの直下のdriverキーの値に "slack" を指定します。そして、read/create/updateのキーに続いて内容に関する配列を収めるのもメールと同様ですが、subjectとbodyのみしか対応していません。なお、「@@フィールド名@@」に対応したテンプレート処理は可能です。データベースのテーブルに用意したメッセージテンプレートには対応していません。
このようにSlack対応は基本的なもので、複雑なことはちょっとしづらいですが、メッセージングの機能のサンプルという意味合いもあります。ソースコードのsrc/php/Messaging/SendSlack.phpを見ていただくと大した内容ではないことお分かりいただけるので、機能アップしたり、あるいは別のメッセージングサービスに対応したクラスを作った場合は、ソースコードを投稿していただけると嬉しいです。
このセクションのまとめ
コンテキストを通じて、データベースから検索した後、データベースの内容を更新した後、新しいレコードを作成した後に、メールを送信することができます。メールの宛先や本文などを定義ファイルで指定できます。メールの本文や宛先は、データベースから得られたレコードのフィールドの値を利用できます。メールの本文をテンプレートとしてテキストファイルで用意して、そこにフィールドを埋め込むといったメールの作成方法も可能です。メールの送信には、同一サーバーにあるSMPTサーバーを利用したり、別のホストに対して認証SMTPで送信することもできます。また、Slackへの投稿も可能であり、メール送るだけでなく汎用的なメッセージの機能の組み込みができます。
5-3マルチクライアントでの同期
2つのクライアントで同一のレコードを表示しているとき、一方のクライアントで変更した結果を別のクライアントでも自動的に反映されるような動作が必要なときもあります。FileMakerでは当たり前に実現しているこうした機能は、Webアプリケーションで実装するには、通信処理などを含めてイチから構築しなければなりません。INTER-Mediatorには、変更結果を伝達する仕組みを搭載しているため、定義ファイルへの記述だけで実現できます。
クライアント間連動の仕組み
通常のWebサーバーとのやりとり、すなわちHTTPは、通信が終了したら切れてしまうという動作を行います。つなぎっぱなしにはできないので、サーバー上のデータが更新されたかどうかは、接続してみないと分かりません。一定時間ごとに接続するとしても、すぐに変更が分かるわけではありません。では、0.5秒ずつ接続するということでは、サーバーの負荷が大きくなり、パフォーマンスを損なう可能性もありますし、バッテリー動作ならば消費電力が大きくなり、不利です。
一方、インターネットの核となるTPC/IPは、通常はつなぎっぱなしができます。逆に言えば、HTTPはつないでは切るという動作をしているわけです。そこで、Webアプリケーションでもサーバーとつないだままにして、変化があったことだけを通知として受け取り、その後に必要なデータ処理を通常のHTTPで行うという仕組みが登場しました。それを、WebSocketと呼びます。INTER-Mediatorでは、Socket-IOというライブラリを利用して、WebSocketの機能を組み込んでいます。
同期処理を稼働させるには、サービスサーバ(『8-6 サービスサーバの役割と稼働』)の稼働が必要であり、既定値では稼働していません。以下の演習では稼働させる最小限の設定を紹介しますが、詳細については後の章で説明します。
管理用テーブルの作成
INTER-Mediatorはどのクライアントにどのテーブルのデータが配布されているのかを、データベースに記録します。そのために表5-3-1のようなテーブルが必要になりますので、アプリケーションでクライアント間同期の機能を利用する場合には、これらと同一名および同一フィルードを持つテーブルを作成してください。PDO対応のデータベースエンジンの場合は、INTER-Mediator/dist-docsファイルにあるサンプルデータベースのスキーマ(sample_schema_*.txtファイル)に、CREATE TABLE等で記述されたSQLがあるので、それを利用できます。なお、registeredcontextテーブルのidフィールドと、registeredpksテーブルのcontext_idフィールドでリレーションシップが設定されています。
テーブル名 | フィールド名 | 型 |
---|---|---|
registeredcontext | id | INT AUTO_INCREMENT, |
clientid | TEXT | |
entity | TEXT | |
conditions | TEXT | |
registereddt | DATETIME | |
registeredpks | context_id | INT |
pk | INT |
演習クライアント間連携の動作を確認する
クライアント間連携の機能を有効にする
演習環境では、クライアント連携の機能は有効になっていません。まず、サーバー側での設定の変更を行います。
apt install nano -y
cd /var/www/html/lib
nano params.php
:
/* Service Server Behavior
* ===================
* Port number and host name for service server */
$notUseServiceServer = false; // Default is TRUE!. It has to set false to work every feature with Service Server.
$activateClientService = true; // Default is FLASE!.
$serviceServerProtocol = "ws"; // The Service Server url components to connect from client.
$serviceServerHost = ""; // "" for public ip address.
:
Webアプリケーションでクライアント間同期を有効にする
以下、Chapter 4で作成した「page01.html/def09.php」のWebアプリケーションに対して、クライアント間同期をできるように設定して動作を確認します。もし、このアプリケーションを作っていない場合には、「http://localhost:9080」で接続したページにある「サンプルプログラム」のリンクをクリックして、「Any Kinds of Samples」にある「Master-Detail Style Page」の「MySQL/MariaDB」の列にある「show」ボタンを押したページで確認をしてください。このサンプルは、ページ全体のコンテキストと、最初の繰り返し部分のコンテキストで同様な設定を行なっています。
編集結果が別のクライアントに伝達されることを確認する
クライアントの動作を確認するために、2つの異なるブラウザーを同時に起動します。SafariとFirefox、あるいはChromeとEdgeなど、INTER-Mediatorの稼働可能なブラウザーを演習環境が稼働しているOSで同時に起動します。これにより、異なるユーザーによる接続と同じ状況になります。なお、ひとつのブラウザーでウインドウが違うという状況では、厳密には「別のクライアント」と同等ではないので、2つのブラウザーを稼働させてください。
レコード追加が別のクライアントに伝達されることを確認する
レコード削除が別のクライアントに伝達されることを確認する
演習のまとめ
- 複数のクライアントで編集結果のリアルタイム同期を実装してあります。利用するデータベースにはいくつかのテーブルが仕様に従って定義されている必要があります。
- サーバ側では、Service Serverが稼働している必要があります。既定の状態では稼働していないので、params.phpファイルを変更して起動するようにします。
- アプリケーションで編集結果の同期ができるようにするためには、コンテキストごとにsync-controlキーの値を指定して、どの更新処理に対して同期をするのかを指定する必要があります。これがない場合は、同期処理は行われません。
同期処理への割り込み
クライアント同期により、伝達してきたクライアント側では、以下の6つの関数をJavaScriptで定義できます。メソッド名を見てわかるように、Create/Update/Deleteのそれぞれのオペレーションの前後つまりBefore/Afterに呼び出されます。クライアントでは、Beforeが呼び出され、更新処理を行い、Afterが呼び出されるようになります。Beforeが付くメソッドでは、引き続く処理を実行するためにはtrueを返します。逆にfalseを返すと、そこでクライアント同期の処理は停止します。引数dは、同期情報に関するオブジェクト(例:{entity: "item", field: ["product_unitprice"], 'justnotify: false, pkvalue: ["3"], value: ["30"]})が設定されて呼び出されます。
INTERMediatorOnPage.syncBeforeUpdate = (d) => {}
INTERMediatorOnPage.syncAfterUpdate = (d) => {}
INTERMediatorOnPage.syncBeforeCreate = (d) => {}
INTERMediatorOnPage.syncAfterCreate = (d) => {}
INTERMediatorOnPage.syncBeforeDelete = (d) => {}
INTERMediatorOnPage.syncAfterDelete = (d) => {}
このセクションのまとめ
複数のクライアント間で、編集やレコード作成、削除の結果をリアルタイムに反映させる仕組みをINTER-Mediatorは持っています。作成したWebアプリケーションは、更新のどの処理を同期させるかを指定することで、そのコンテキストに対する更新処理を別のクライアントに同期ができるようになります。
5-4JavaScriptコンポーネントの利用
現在、Webアプリケーションの機能を拡張するために、JavaScriptで作られた部品(コンポーネント)が盛んに使われています。INTER-Mediatorでも、JavaScriptのコンポーネントを利用して、データベースの内容を表示したり、あるいは修正結果をフィールドに描き戻すことができます。これらの機能の基本的な利用方法を説明します。また、ファイルのアップロードとアップロードした結果を表示する方法についてもこのセクションで説明します。
JavaScriptのコンポーネントを利用する
多種多様なJavaScriptのコンポーネントがオープンソースで配布されています。ライセンス形態はMIT Licenseのものも多く、気軽にサイトで利用している人も多いでしょう。代表的なものといえばjQueryやあるいはそれをベースにしたユーザーインターフェース素材のjQueryUIなどがあります。まず、INTER-Mediatorでは、これら別途開発されたコンポーネントを活用し、テキストフィールドなどと同様に、データベースの値を表示したり、あるいは編集した結果をデータベースに更新する仕組みを組み込むことができます。既存のコンポーネントを利用する場合は、そのコンポーネントに対する「アダプター」を作成しなければなりません。INTER-Mediator Ver.10現在では、jQueryUIのDatePickerやFile Upload、HTMLエディターのTinyMCE、ソースコードエディターのCodeMirrorのアダプターなどが付属しています。これら以外の素材を利用する場合には、独自にアダプターを開発しなければなりません。アダプターの開発方法は、『6-6 JavaScriptコンポーネント用のアダプターの開発方法』で説明をします。INTER-Mediatorには独自のユーザーインターフェース用部品も搭載されています。
これらのJavaScriptコンポーネントを利用するには、タグ要素にdata-im-widget属性を記述します。この属性の値は、それぞれのアダプターで定義された文字列を指定します。なお、設定可能なタグ要素はなんでもいいわけではなく、原則として、そのコンポーネントごとに決められています。例えば、jQueryUIのDatePickerは、テキストフィールドの要素に対して指定します。
実際の試用方法は、演習で具体的に説明しましょう。
アップロードしたファイルのパスの扱い
ファイルのアップロードを、『5-4 JavaScriptコンポーネントの利用』などのJavaScriptコンポーネントを使った方法で実現したとき、レコードに対してユーザー単位でのアクセス権を設定している場合の動作に関する設定が、params.phpに設定する変数$uploadFilePathModeです。例えばコンテキスト「files」があり、キーフィールドが「id」で、idフィールドの値が「56」のレコードに対してファイルのアップロードをしたとします。その時のアップロードコンポーネントにバインドしているフィールドが「path」であったとします。すると、ファイルは定義ファイルのIM_Entryに記述した2つ目の引数にあるmedia-root-dirキーのパスに加えて、Linuxサーバーの場合は「files/id=56/path」という相対パスを付与したディレクトリにアップロードしたファイルを保存します。この時、$uploadFilePathModeを未指定、あるいは""にすると、パスの区切り文字列以外はPHPのurlencode関数でエンコードします。したがって、フィールド名が日本語だと、パスにその日本語が見えないことになります。これは、UTF-8での冗長なエンコーディングによるディレクトリを遡る処理を許してしまうセキュリティホールを回避するものです。INTER-Mediatorでは相対パスに含むドット文字はアンダーラインに変換しますが、冗長なエンコーディングによりそれが回避される可能性があるので、このような措置にしています。しかしながら、ディレクトリ名を日本語で見たいという場合もあります。その時は、$uploadFilePathModeに"assjis"あるいは"asucs4"を指定してください。そうすれば、mb_stringを利用して、文字列を一度Shift JISあるいはUCS-4に変換し、さらにUTF-8に戻すことによって、不正であるとされている冗長なエンコーディングの文字が正しいエンコードになります。こうして安全かつ日本語でディレクトリ名が見える状況にもできるようになっています。
演習ファイルアップロードのコンポーネントを利用する
INTER-Mediatorに組み込まれているファイルのアップロードのコンポーネントの利用方法を説明します。なお、アップロードにおいては、いろいろな準備が必要ですし、アップロードしたファイルを参照する方法も知っておく必要があります。これらをまとめて、この演習で説明をします。
また、PHPの環境上の制限で現在の標準設定では1.5MB程度が上限となり、演習環境はその設定を変更していません。それ以上のファイルをアップロードしようとしても、制限を超えているというメッセージが表示されてアップロード作業は完了しません。アプリケーションを実際に作るとき、精細な写真を貼付したい、あるいは動画を保存しておきたいときなど、業務上、どうしても大きなファイルを添付しなければならないという場合は、PHPの環境設定ファイルを書き直すなどの対応が可能です。方法は、『9-4 INTER-Mediatorを利用する開発プロセス』で説明します。
2つのコンテキストを定義ファイルに定義
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
ページファイルの作成と表示
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Loading JQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<!-- Loading Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Loading JQuery UI FileUpload -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/css/jquery.fileupload.min.css"/>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-process.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-image.min.js"></script>
<!-- Loading INTER-Mediator -->
<script src="def13.php"></script>
<script src="/vendor/inter-mediator/inter-mediator/node_modules/inter-mediator-plugin-jqueryfileupload/index.js"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr>
<th>File Upload</th>
<th>Path</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="testtable@text1" data-im-widget="jquery_fileupload"></td>
<td data-im="testtable@text1"></td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
実際にファイルをアップロードしてみる
アップロードした結果の確認
ファイルがアップロードされた結果を、演習環境上で確認をします。
cd /var/www
ls -l
find testtable
アップロードしたファイルの履歴を残す
ここまでの方法では、あるレコードのあるフィールドに対して、アップロードしたファイルのうち、最後のファイルへのパスだけがデータベースに残っていました。さらに発展させて、ファイルをアップロードした履歴も残すようにします。
<!DOCTYPE html>
<html lang="ja">
<head>
:
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr>
<th>File Upload</th>
<th>Path</th>
<th>fileupload</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="testtable@text1" data-im-widget="jquery_fileupload"></td>
<td data-im="testtable@text1"></td>
<td>
<table>
<tbody>
<tr>
<td data-im="fileupload@path"></td>
</tr>
</tbody>
</table>
</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
cd /var/www
find testtable
# find testtable
testtable
testtable/id=3
testtable/id=3/text1
testtable/id=3/text1/P1100099_3235.JPG
testtable/id=3/text1/child-msyk_4518.png
testtable/id=3/text1/NII-2_8482.jpg
testtable/id=2
testtable/id=2/text1
testtable/id=2/text1/313_6317.jpg
アップロードしたファイルをページに表示する
ファイルがアップロードできるようになりましたが、それだけではファイルが取り扱えません。今度は、コンポーネントでアップロードしたファイルを、Webページの中で表示する方法を説明します。なお、この方法は、アップロードのコンポーネントを使わないでアップロードしたファイルについても適用できる手法です。
<!DOCTYPE html>
<html lang="ja">
<head>
:
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr>
<th>File Upload</th>
<th>Path</th>
<th>fileupload</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="testtable@text1" data-im-widget="jquery_fileupload"></td>
<td data-im="testtable@text1"></td>
<td>
<table>
<tbody>
<tr>
<td data-im="fileupload@path"></td>
<td><img style="height: 50px" src="def13.php?media="
data-im="fileupload@path@#src">
</td>
</tr>
</tbody>
</table>
</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
演習のまとめ
- ファイルのアップロードのためのコンポーネントが用意されており、data-im-widget属性に「jquery_fileupload」を指定すると、利用できます。ただし、アップロードできるファイルサイズの上限は演習環境では約2MBまでです。この制限を大きくすることもできます(『9-4 INTER-Mediatorを利用する開発プロセス』を参照)。
- data-im-widget属性は、テキストを保存するフィールドにバインドしたタグ要素に指定します。そのフィールドにはアップロードしたファイルへのパスが設定されます。
- アップロードされたファイルは、media-root-dirキーで指定したパス以下、コンテキスト名、レコードを特定する条件、フィールド名と続くディレクトリが作られ、そこに保存されます。
- アップロードされたファイルのファイル名に4桁のランダムな値が付与され、同一ファイル名を持つファイルがアップロードされても上書きされないようにしています。
- アップロードの履歴を別テーブルに残すこともできます。
ファイルアップロードのその他の機能
ファイルのアップロード履歴を、演習ではfileuploadコンテキストのテーブルに残していました。このテーブルの構成としては、pathという名前のテキスト型フィールドが必要ですが、その他は自由に設定可能です。relationキーによるリレーションシップが定義されていますが、演習のような設定をした場合には、fileuploadコンテキストのテーブルには、外部キーとなるf_idフィールドが必要です。また、この演習では設定していませんが、レコード作成日時を自動的に設定するタイムスタンプのフィールドを確保し、現在の日時を既定値にすれば、ファイルをアップロードした日時が分かります。
jquery_fileuploadコンポーネントは、ファイルを選択したときにプレビューが表示されるようになっていますが、もし、プレビューを表示せず、ファイル名だけを表示したいのであれば、JavaScriptのプロパティで指定可能です。「IMParts_Catalog.jquery_fileupload.isShowPreview = false」といったコードを、INTERMediatorOnPage.doAfterConstructで実行する関数の中に入れておけば良いでしょう。
その他の付属のコンポーネント
ファイルのアップロードは演習で見たように、jQueryなどの別のソフトウェアのインストールが必要です。これらの使用方法については、INTER-Mediatorにあるサンプルを参照してください。ディレクトリはレポジトリのルートからだと、samples/Sample_webpage/で、表5-4-1〜表5-4-7のようなファイルを参照してください。サンプルファイルのリンクページにも、これらのサンプルへのリンクは存在します。TinyMCEなど、アダプターが利用するコンポーネント本体は、ページファイルのヘッダーで、スタイルシートファイルやJavaScriptのファイルを、適切なパスを指定して読み込む必要があります。パスを指定しますので、必ずしもページファイルと同じ階層に存在する必要はありません。別のディレクトリにあっても参照ができれば問題はありません。また、コンポーネント自体は別レポジトリで管理しています。
コンポーネント | TinyMCE |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-tinymce |
data-im-widgetの値 | tinymce |
ページファイル | samples/Sample_webpage/tinymce_MySQL.html |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | CodeMirror |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-codemirror |
data-im-widgetの値 | codemirror |
ページファイル | samples/Sample_webpage/codemirror_MySQL.php |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | jQuery DatePicker |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-jquerydatepicker |
data-im-widgetの値 | jquery_datepicker |
ページファイル | samples/Sample_webpage/jquery_datepicker_MySQL.php |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | flatpickr(日付と時刻を同時に設定可能なコンポーネント) |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-flatpickr |
data-im-widgetの値 | flatpickr |
ページファイル | samples/Sample_webpage/flatpickr_MySQL.html |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | jQuery DatePicker |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-jqueryfileupload |
data-im-widgetの値 | jquery_fileupload |
ページファイル | samples/Sample_webpage/fileupload_jQuery_MySQL.html(他にもあり) |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | Popup Selector(ポップアップメニュー。独自開発) |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-popupselector |
data-im-widgetの値 | popupselector |
ページファイル | samples/Sample_webpage/popuselector_MySQL.html |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | JSON Formatter(JSONを整えて表示する。独自開発) |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-jsonformatter |
data-im-widgetの値 | jsonformatter |
ページファイル | なし |
定義ファイル | なし |
コンポーネント | Mermaid |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-mermaid |
data-im-widgetの値 | mermaid |
ページファイル | samples/Sample_webpage/mermaid_MySQL.html |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
コンポーネント | QRCode |
---|---|
レポジトリ | https://github.com/inter-mediator/inter-mediator-plugin-qrcode |
data-im-widgetの値 | qrcode |
ページファイル | samples/Sample_webpage/qrcode_MySQL.html |
定義ファイル | samples/Sample_webpage/include_MySQL.php |
メディアファイルの内容の取得
定義ファイルは、通常はフレームワーク自体をページファイルに送り込むことや、データベースアクセスに利用しますが、他にもさまざまな機能があります。そのうちのひとつが、ファイルの内容を取り出す仕組みです。HTMLでは、IMGタグによる画像や、PDFファイルへのリンクといった用途に使うことを想定しており、パスを与えて、そのファイルの中身をMIMEタイプなどとともにクライアントに返すといった動作を行います。基本的にはリスト5-4-1のような記述を行います。mediaというキーでパラメーターを指定するということです。
一般的な記述:定義ファイルへのパス?media=ファイルへのパス
例:def13.php?media=shot0001_3923.png
ファイルへのパスが「http://」「https://」で始まるURLの場合には、そのURLから得られた結果をそのまま返します。パスが「class://」で始まる場合には、その後に自分で作成したPHPのクラスのプログラムを実行できます。これについては『Chapter 8 サーバーサイドでのプログラミング』で解説をします。
上記のプロトコル以外は、ファイルへのパスとみなします。そして、media-root-dirキーで指定したパスに続いて、media=以降のパスで構成される絶対パスのファイルを取り出して、その内容を返します。なお、media=が「/fmi/xml/cnt」の場合、つまりFileMaker Serverを使っていて、オブジェクトフィールドを指定した場合のみ、FileMaker ServerへのURLに変換して、オブジェクトフィールドの内容を画像等で取り出すといった動作を行います。
このセクションの演習で行ったように、ファイルアップロードのコンポーネントでアップロードしたファイルのパスが相対パスで記録されていれば、そのフィールドの値をmedia=の値に指定すればOKです。fileuploadコンテキストのようなアップロード履歴を残すテーブルは相対パスですが、演習でいえば、testtableコンテキストのtext1フィールドは絶対パスで記録されてしまっています。このままではIMGタグのsrc属性に指定をしたい場合には少し不便なので、この仕様は将来変更する可能性もあります。
FileMakerのオブジェクトフィールドへのアップロードと画像表示
FileMakerを利用している場合、オブジェクトフィールドへ画像を保存することは一般的です。したがって、オブジェクトフィールドに画像を入れることを前提として、Webアプリケーションも作成したいと考えるでしょう。INTER-Mediatorでは、ファイルアップロードのコンポーネントが直接オブジェクトフィールドにデータを入力できますし、オブジェクトフィールドの画像をIMGタグ要素で画面に表示することもできます。
このセクションの演習で作った一連のファイルに対して、オブジェクトフィールドを利用する場合にはどのようにすればよいかを説明します。まず、データベース「TestDB」について確認します。testtableレイアウトには、vc1という名前のオブジェクトフィールドがあります(図5-4-1)。このオブジェクトフィールドに画像等を入力するものとします。
このオブジェクトフィールドには計算値自動入力の設定がなされています。フィールド定義を調べるには、「ファイル」メニューの「管理」から「データベース」を選択して、データベースの管理ダイアログボックスを表示します(図5-4-2)。この式については、FX.phpを利用している場合に、アップロードとダウンロードをうまく整合させるために設定した式です。FileMaker Data APIを利用する場合にはこの式の設定は必要ありません。
ここから、定義ファイルとページファイルを用意します。それぞれ、def14.phpとpage14.htmlのファイルを利用しますが、別の番号のものでも構いません。まず、定義ファイルのContextsではひとつのコンテキストを定義します。初期状態では、queryとsortに設定があるので、それを消しておきます。そして、図5-4-3のように、nameとtableとviewを「testtable」、keyを「id」、recordsを「10」、maxrecordsを「100」、pagingを「true」、repeat-controlを「confirm-insert confirm-delete」としておきます。
続いて、定義ファイルエディタの最初のところにあるShow Allボタンをクリックして、項目をすべて出しておきます。そして、図5-4-4のように、testtableコンテキストのFile Uploadingのところで「追加」ボタンをクリックして新たな行を追加し、fieldに「vc1」、containerに「true」を入力しておきます。
さらに、Optionsの設定では、図5-4-5のように、media-root-dirに「/var/www」と設定をしておきます。
Database Settingsでは、図5-4-6のように、db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。
ページファイルをリスト5-4-2のようにします。ファイルアップロードのコンポーネントをdata-im-widget属性で指定するタグ要素は、vc1フィールドにバインドします。そして、vc1フィールドに入力されている画像を表示するには、vc1フィールドの値をmedia=の後につなげます。オブジェクトフィールドは、カスタムWeb経由でデータを得ると画像等のバイナリデータではなく、フィールドに入力することが可能なURLの一部分が得られます。それを定義ファイルの引数に与え、定義ファイルから先のINTER-Mediatorの内部で正しいURLを構築して画像データなどを得ています。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Loading JQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<!-- Loading Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Loading JQuery UI FileUpload -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/css/jquery.fileupload.min.css"/>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-process.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/10.32.0/js/jquery.fileupload-image.min.js"></script>
<!-- Loading INTER-Mediator -->
<script src="def14.php"></script>
<script src="/vendor/inter-mediator/inter-mediator/node_modules/inter-mediator-plugin-jqueryfileupload/index.js"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr>
<th>File Upload</th>
<th>Path</th>
<th>fileupload</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="testtable@vc1" data-im-widget="jquery_fileupload"></td>
<td data-im="testtable@vc1"></td>
<td>
<img style="height: 50px" src="def14.php?media=" data-im="testtable@vc1@#src">
</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
ページファイルを表示して、画像ファイルをアップロードしてみます。アップロードすれば、ページの中にその画像が見えているはずです。表示されている画像のパスは、FileMakerのコンテナフィールドの情報へのURLです。
>Amazon S3にファイルを保存する
ファイルをローカルのディレクトリに保存する方法をこれまでは示しましたが、ファイルをAmazon S3(Simple Strage Service)に保存する方法を説明します。ローカルのディレクトリに保存する機能において、ローカルに保存するところでS3のAPIを呼び出して、ファイルをそちらに保存します。その結果、S3からは、httpsで始まるURLが返され、そのURLを利用してファイルの内容を取り出すことができます。ここで、INTER-Mediatorは、S3から返すURLの先頭のhttps://を、s3://に置き換えてフィールドに記録します。つまり、パスのURLの最初の文字列から「S3に保存しているオブジェクトである」ことが判明できるようになっています。
このように、S3へのアクセスは完全にINTER-Mediatorの内部で処理されるので、定義ファイルの一部を変えるだけでS3を保存領域として使うようになります。なお、EC2などでWebサーバを稼働させて、ファイルはS3に残すことも可能ですが、オンプレミスのWebサーバでファイルはS3に保存することも可能です。INTER-Mediatorに対しては、S3への利用のためのさまざまな設定情報を、リスト5-4-3に示すようなparams.phpファイルの変数に定義します。原則、すべての設定が必要ですが、アカウントに関しては$s3AccessProfileを指定するか、$s3AccessKeyと$s3AccessSecretを指定する方法があります。リージョンとバケット名(S3の記憶領域につける名前)は、実際に使用する状況に合わせます。$applyingACLは決められたキーワードを指定する必要があります。params.phpファイルに指定可能な文字列が記載されているのでそれを参考にしてください。$s3urlCustomizeは通常はtrueにして、URLの先頭のプロトコル部分を「s3」に置き換える状態にしておきます。
$accessRegion = "ap-northeast-1"; // いわゆる東京リージョン
$rootBucket = "inter-mediator-developping"; // バケット名
$applyingACL = "bucket-owner-read"; // オブジェクトのアクセス権
$s3AccessProfile = "im-develop"; // プロファイル(後述)
$s3AccessKey = "AKIAXXXXXXXXXXXXXXXX";
$s3AccessSecret = "XXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXX";
$s3urlCustomize = true; // 保存時に得られたURLを、httpsからs3に変更
S3を利用するアカウントに関しては、IAMでアカウントを作成して、S3関連のアクセス権を設定しておきます。「AmazonS3FullAccess」であれば確実に読み書きができますが、管理権限も与えられるので不安がある場合にはもう少し絞った権限に設定しておきます。IAMの管理ページにある「セキュリティ認証情報」の「アスセスキー」の箇所で、アクセスキーとシークレットを発行し、それを利用します。シークレットは作成時点でしか参照できないので、何らかの方法で確実に手元にコピーを残すことを心がけましょう。
$s3AccessKeyと$s3AccessSecretをparams.phpに指定すればS3を利用できるのですが、ファイルにシークレットが入り込むのを避けたい場合には、AWSのアカウントのプロファイルの機能を利用してください。そうすれば、ファイルには$s3AccessProfileでプロファイル名を指定すれば良く、$s3AccessKeyと$s3AccessSecretは指定する必要がありません。AWSのプロファイルは、ホームディレクトリにある.awsファイルにあるconfidentialsというテキストファイルで、内容は以下のような形式です。この形式を連続して記録することもできます。[ ] の部分がプロファイル名になります。
[imapp_account]
aws_access_key_id = AKIAXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXX
プロファイルは、エディタで手で記述しても構いませんが、コマンドラインの「aws configure - -profile」等で指定できます。これら、アカウントの作成やプロファイルの運用については、詳しくはAWSのドキュメント等を参照して、効率の良い方法を模索しましょう。なお、.awsディレクトリは、ホームディレクトリにあればよく、Macでローカル運用する分には普通に自分のホーム直下に.awsディレクトリを作れば問題ありません。ところが、状況によって、.awsディレクトリを探す手段が、私たちが思い浮かべるホームではないところを利用する場合もあります。例えば、EC2でUbuntu Server 20を使う場合、ユーザがwww-dataでありホームは/var/wwwのはずなのですが、.awsディレクトリは、ルートの/から探します。エラーメッセージを読めばわかるのですが、最初はともかくエラーになってしまいます。作った.awsディレクトリをルートに移動させておけば問題ありませんが、余計なところで悩みが増える可能性もあります。
S3はバケットそれぞれにパブリックアクセスをブロックする設定があります。管理ページでは、バケットの「アクセス許可」のタブに設定項目があり、「編集」をクリックすると、図5-4-8のような画面が出てきます。ここで蓄積したファイルを別の用途で使うということになると、いろいろ考えないといけないかもしれませんが、INTER-Mediatorの中で画像などのファイルを扱うだけであれば、このように「パブリックアクセスをすべてブロック」している状態で問題はありません。
params.phpの設定ができれば、ページファイル、定義ファイルを作ります。ファイルをアップロードするコンポーネントは、通常の場合と同様にdata-im-widget="jquery_fileupload"を指定するだけです。ファイルのダウンロードでは、aタグやimgタグを利用して、URLにフィールドの内容を指定します。なお、URLを保存するフィールドは、日本語のファイル名などでは1Kや2Kは軽く行ってしまうので、フィールドの型はTEXTにしておきましょう。定義ファイルでは、リスト5-4-5のように、コンテキスト定義のfile-uploadキーの値に配列を指定します。配列では、containerキーのみ指定して、そこに「S3」と指定します。この配列には、アップロードの履歴を残す場合に、パスやURLを受け入れるフィールドをfieldキーで、履歴テーブルのコンテキスト名をcontextキーで指定しますが、履歴を残さない場合にはこれらの指定は不要です。残す場合には指定は必要です。いずれにしても、containerキーで「S3」という値が指定されているようにする必要があります。
'file-upload' => [
['container' => 'S3',],
],
Dropboxにファイルを保存する
アップロードしたファイルをローカルのディレクトリに保存するだけでなく、Dropboxにもアップロードすることができます。Dropboxは、デスクトップでの利用が有名ですが、APIがあり、フォルダの中などに見えている領域にアップロードあるいはそこからのダウンロードが可能です。ただし、APIを利用するためのトークンの利用が2021年の改訂以降ちょっと複雑になっているため、ここではともかくINTER-MediatorでDropboxを利用できるようにするための最短距離は追いかけておきます。以前は無期限のトークンを発行できたのですが、現在は比較的期限が短いトークンの発行しかできなくなりました(現状は4時間で無効になる)。結果的に、いくつかの手順を経て「リフレッシュトークン」というトークンを取得し、それを元にAPIを利用する「アクセストークン」を発行します。アクセストークンが無効になると、リフレッシュトークンを元にアクセストークンを再発行し、それが無効になるまで再利用します。都度都度リフレッシュトークンからアクセストークンを発行すれば良いと思うかもしれませんが、それがあまりに頻繁だと発行をキャンセルしてしまうようで、ともかくアクセストークンは再利用しないといけないということです。INTER-Mediatorでは、リフレッシュトークンを登録すれば、あとは自動的に処理が進むようになっています。そのために、PHPのライブラリとしてmsyk/dropbox-api-shortlivedtokenというものを作っていますが、これはINTER-MediatorでのDropbox対応のために作ったライブラリです。ちなみに、なぜかDropboxは公式にはPHPのライブラリをリリースしておらず、第三者によるものしかありません。実装する時にspatie/dropbox-apiというライブラリを使い始めたのですが、アクセストークンの保持管理ができないため、このライブラリをベースにして作ったものがmsyk/dropbox-api-shortlivedtokenです。
まず、リフレッシュトークンの取得方法について、概要を説明します。詳細な方法はDropboxのドキュメントを参照してください。最初に、Dropbox Developerのアプリコンソールでアプリを生成します。すると、AppKeyとAppSecretがページ上に見えます。ここからいくつかのAPIコールなどを経てリフレッシュトークンは発行できますが、手順としてはあまり簡単ではありません。ひとつの方法としては、Dropbox Api Short-Lived tokens and refresh tokens — Spring + Java Applicationに記載されているJavaのプログラムを利用して、リフレッシュトークンを取得する方法があります。Javaに関する知識が必要になるかもしれませんが、Eclipse等でプログラムを稼働すれば、AppKeyとAppSecretからリフレッシュトークンの取得が可能です。途中、コンソールの指示に従っての操作が入り、ブラウザを操作してコードを得て入力するなどの作業が必要になります。
ここまでの作業で得られたAppKey、AppSecret、リフレッシュトークンの3つは、リスト5-4-6のように、params.phpファイルの変数として定義しておきます。これらから発行されるアクセストークンは、$dropboxAccessTokenPathで指定したファイルに記録します。したがって、このファイルは、Webサーバのアカウントによって書き込み可能になっている必要があります。例えば、Ubuntuでは、www-dataによって書き込み可能になっている必要があります。/var/www以下に適当なファイルを作って、アクセス権の設定をコマンド等で行なっておきます。Dropbox上で、$rootInDropboxで指定されたパス以下にファイルが書き込まれます。なお、当然のことですが、アプリを生成したときに使われたDropboxの領域内に、ファイルは保存されます。
$dropboxAppKey= 'xxxxxxxxxxxx'; // 発行されたAppKey
$dropboxAppSecret= 'xxxxxxxxxxxx'; // 発行されたAppSecret
$dropboxRefreshToken= 'xxxxxxxxxxxxxxxxxxxx'; // リフレッシュトークン
$dropboxAccessTokenPath= '/tmp/dropbox-access-token.txt'; // アクセストークンを記録するファイル
$rootInDropbox= '/'; // Dropbox上のファイルを保存場所へのパス
params.phpの設定ができれば、ページファイル、定義ファイルを作ります。ファイルをアップロードするコンポーネントは、通常の場合と同様にdata-im-widget="jquery_fileupload"を指定するだけです。ファイルのダウンロードでは、aタグやimgタグを利用して、URLにフィールドの内容を指定します。定義ファイルでは、リスト5-4-7のように、コンテキスト定義のfile-uploadキーの値に配列を指定します。配列では、containerキーのみ指定して、そこに「S3」と指定します。この配列には、アップロードの履歴を残す場合に、パスやURLを受け入れるフィールドをfieldキーで、履歴テーブルのコンテキスト名をcontextキーで指定しますが、履歴を残さない場合にはこれらの指定は不要です。残す場合には指定は必要です。いずれにしても、containerキーで「Dropbox」という値が指定されているようにする必要があります。
'file-upload' => [
['container' => 'Dropbox',],
],
CSVファイルのアップロード
ファイルのアップロードをしたとき、そのファイルの中身をCSV等の表形式のデータとして認識して、レコードの追加や更新等ができるようになっています。そのためには、Post Onlyモードのページ(『3-4 入力専用のPost Onlyモード』を参照)を、いくつかのルールに基づいて作成することです。ページファイルのひとつのエンクロージャーをPost Onlyにして、その中に、jquery_fileuploadのコンポーネントを配置します。そのコンポーネントのdata-im属性は「コンテキスト@_im_csv_upload」と指定します。ターゲット指定のコンテキストは、定義ファイルに存在するコンテキストの名前を指定し、実際にこのコンテキストに対してレコード作成等が行われます。ターゲットしてのフィールド名は、「_im_csv_upload」で固定です。リスト5-4-8はその例です。jquery_fileuploadを利用するのであれば、ヘッダにいくつかの読み込みが必要です。『5-4 JavaScriptコンポーネントの利用』の演習に示したHTMLのコードを参考にしてください。
<div data-im-control="post enclosure">
<div data-im-control="repeater">
<span data-im="testtable@_im_csv_upload"
data-im-widget="jquery_fileupload"></span>
<button data-im-control="post">送信</button>
</div>
</div>
定義ファイルでの指定例をリスト5-4-9に示します。Post Onlyモードに展開されたコンテキストは存在しなければなりませんが、この情報からテーブル名等を取得するので、nameは取り込みを行うテーブル名そのものにするのが良いでしょうし、keyについては主キーを正しく指定します。file-uploadは、アップロードですが指定は不要でしょう。アップロードファイルを残すという処理は組み込まれていませんので、ファイルを残す情報は不要になります。そして、オプションとして、importキーに対する設定を組み込みます。この設定は、オプションだけでなくコンテキスト定義やparams.phpでも指定が可能です。設定可能なキー等はこの後に詳細に説明します。
// コンテキスト定義での定義例
["name" => "testtable",
"key" => "id",
"file-upload" => [["container" => "FileSystem",],], //省略可
"post-reconstruct" => true, ...
],
// オプションの配列(IM_Entryの第2引数)での指定
"import" => [
"1st-line" => true,
"skip-lines" => 0,
"use-replace" => true,
"convert-number" => ["num1", "num2", "num3"],
],
キーワード | 動作 | params.php |
---|---|---|
1st-line | 指定なし、あるいはtrueなら1行目をフィールド指定行と見なす。フィールド指定行がない場合には、'フィールド1,フィールド2, ....'といった文字列を指定する | $import1stLine |
skip-lines | 先頭から指定した数の行を無視する。指定なしの場合は0と見なす。フィールド指定行は、ここで指定した行数を省いた最初の行と見なす。 | $importSkipLines |
format | 読み込むファイルのフォーマットとして"CSV"あるいは"TSV"を指定する。省略すると"CSV" | $importFormat |
use-replace | データベースがMySQLの場合、trueにすると、INSERTではなくREPLACEコマンドを利用してファイルの各行のレコードを挿入あるいは更新する。MySQLでない場合は常にINSERTを利用する。省略するとfalseと見なす。 | $useReplace |
convert-number | 数値変換を行うフィールド名を配列で指定する | $comvert2Number |
convert-date | 日付への変換を行うフィールド名を配列で指定する | $comvert2Date |
convert-datetime | 日付時刻への変換を行うフィールド名を配列で指定する | $convert2DateTime |
まず、1st-lineキーは、読み込むテキストファイルの1行目を指定します。指定なしの場合には、テキストファイルの1行目をフィールド名として認識して、各行の項目を、1行目に対応するフィールド名のフィールドに入力します。1st-lineは文字列で指定し、指定をすればテキストファイルの1行目を、指定した文字列に置き換えて処理をします。通常は1行目からをデータとして読み込むので、1行目にすでにデータがある場合は、フィールド名の指定を1st-lineで指定する必要があります。1行目を置き換えて読み込みたい場合は、1st-lineを指定しつつ、skip-linesに1を指定します。
既定値はCSV形式と仮定します。数値はそのまま、文字列は " " あるいは ' ' で囲まれているものです。改行は、CR、LF、CR+LFのいずれも対応します。もちろん、文字の中にエスケープして入っているダブルクォートなども正しく認識します。ここで、formatキーでTSVを指定すると、区切り文字がタブであると認識します。いずれの場合も、テーブルに対してINSERTステートメントでレコードを作成するのが基本です。use-replaceをtrueにした場合、データベースがMySQLであれば、既存のレコードの置き換えを行います。つまり、MySQLのREPLACEステートメントを利用してテキストファイルの1行を挿入します。REPALCEは、主キーフィールドを手掛かりにして、存在しないレコードは新規作成、存在するレコードは更新する動作です。実行時にどのフィールドを照合するかは指定できないので、データベーススキーマで最初からキーフィールドの指定が必要になります。
convert-number、convert-date、convert-datetimeは、いずれもフィールド名の配列を指定します。そのフィールドのデータに関して、数値、日付、日付時刻の形式に合ったデータに変換して、データベース処理のステートメントに含めるようにします。数値の場合、「3,200」のようなものでもエラーが出る場合があるので、そうした処理に対応しています。
このセクションのまとめ
JavaScriptで作られたさまざまなソフトウェアコンポーネントを利用する仕組みを持っています。しかしながら、利用するには、アダプターの開発が必要になります。INTER-Mediatorには、TinyMCE、CodeMirror、jQuery FileUploadなどのアダプターが付属しています。定義ファイルは、画像ファイルなどの内容を取り出すことにも利用できます。
5-5クロステーブルの作成
クロステーブルは、表があって、一番上の列と、一番左の列がラベルとなっており、そのラベルの交差するセルには、ラベルに関連するデータが表示されるというものです。例えば、一番上の列には自社の「支店」が並び、一番左の列には年月が並ぶとすると、その交差するセルには、特定の支店の特定の年月の売り上げが見えるというものです。INTER-Mediatorではこうしたテーブルをデータベースの値を利用して作成することができます。
クロステーブルに必要な記述
通常、INTER-Mediatorはひとつのコンテキストをページ上に展開してページ合成を行いますが、クロステーブルは3つのコンテキストを使用します。そのため、定義ファイルには3つの異なるコンテキストを定義しなければなりません。それぞれ、「列見出し用コンテキスト」「行見出し用コンテキスト」「交差セル用コンテキスト」と名付けることにします。クロステーブルの見出しは固定的なものではなく、データベースのマスターテーブル等から取り出す仕組みにしてあります。そのため、汎用性は高いですが、現状では、単に数字の100から150までのような見出しを利用するのが少し面倒です。この方法は、Samples/Sample_ExtensibleにあるYearMonthGen.phpファイルおよびそれを使用している定義ファイルを参照してください。
行見出し用および列見出し用のコンテキストは通常と変わりありません。1レコードがひとつの見出しセルに展開されます。これに対して、交差セル用のコンテキストでは、relationキーの値で2つの要素を定義します。ひとつ目は行見出し用のコンテキストとの関係、2つ目は列見出し用コンテキストとの関係、すなわち対応するフィールドが何かを記述します。設定の中にあるoperatorキーは無視され、イコールによる関係しか扱えません。コンテキスト指定のポイントを示したのが、リスト5-5-1です。なお、コンテキストの中にはqueryやsortキーでの検索条件や並べ替え設定があっても構いません。必須なことはコンテキスト定義が3つあることと、交差セル用コンテキストにおいて、2つの要素を持つrelationキーの値があることです。なお、ここでは説明を分かりやすくするために、nameキーと同一のテーブルがあるといった状況を想定してください。
array( // 列見出し用コンテキスト
"name" => "item",
"key" => "id",
),
array( // 行見出し用コンテキスト
"name" => "customer",
"key" => "id",
),
array( // 交差セル用コンテキスト
"name" => "salessummary",
"key" => "id",
"relation" => array(
array( // 列見出し用コンテキストとの関連
"foreign-key" => "item_id",
"join-field" => "id",
"operator" => "=",),
array( // 行見出し用コンテキストとの関連
"foreign-key" => "customer_id",
"join-field" => "id",
"operator" => "=",),
),
),
一方、ページファイルでは、TABLEタグ要素を利用したテーブルを利用します。エンクロージャーとなるTBODYタグ要素のdata-im-control属性には「cross-table」という値を設定します。その中には2行2列のセルだけを入れます(図5-5-1、リスト5-5-2)。data-im-control属性に「cross-table-sum」と設定すれば、クロステーブルの右側と下側に合計を求めるセルをさらに追加します。セルはそれぞれ、THでもTDでもどちらのセルでも構いません。リスト5-5-2では、各セルの中にdata-im属性によってターゲット指定が記述されています。ここで、Ⓑのセルは行見出し用コンテキスト、Ⓒのセルは列見出し用のコンテキストが展開されます。したがって、これらのセルでは、コンテキスト定義に存在するコンテキスト名およびその中に存在するフィールド名を記述する必要があります。そして、Ⓓのセルには、交差セル用コンテキストに対応したターゲット指定を記述しますが、セルを埋めるルールは、この後の『クロステーブル生成の仕組み』で説明をします。なお、Ⓐのセルは、ページ合成後もテーブルの左上のセルとして配置されます。
<table>
<tbody data-im-control="cross-table">
<tr>
<th></th>
<th>
<div data-im="item@id"></div>
<div data-im="item@name"></div>
</th>
</tr>
<tr>
<th>
<div data-im="customer@id"></div>
<div data-im="customer@name"></div>
</th>
<td>
<div data-im="salessummary@qty"></div>
<div data-im="salessummary@total"></div>
</td>
</tr>
</tbody>
</table>
クロステーブル生成の仕組み
クロステーブルを合成する仕組みを解説しましょう。コンテキストが3つもあるとややこしく思われるかもしれませんが、それぞれの見出しを合成した上で、交差セルを埋めるという処理が全体的な流れになります。その流れが理解できれば、動作の見通しもつきやすいでしょう。
まず、ページファイルでのクロステーブル部分の初期状態は図5-5-1のようなものです。この時、TBODY内部のセルを一度全部取り除きます。そして、ⒶⒷⒸⒹの4つのTHないしはTDタグ要素を別途記録しておきます。最初に1行目のTRタグ要素を生成し、その子要素としてⒶのセルを追加します(図5-5-2)。ここでのTRタグは、ページファイルにあったもではなく、プログラムで単に生成したものです。Ⓐのセルは単に複製して子要素にするだけですので、この中にターゲット指定を記述しても、データベースの内容を展開することはありません。
続いて、テーブルの1行目に列見出しを作成します。図5-5-3のように、1行目のTRタグ要素をエンクロージャー、Ⓑのセルをリピーターとして、ページ合成を行います。ここではⒷのセルの中身を解析して、ターゲット指定の設定を集めて、一番多く使われているコンテキスト名を決定し、そのコンテキスト名の定義に基づいてデータベース処理を行います。つまり、通常のエンクロージャー/リピーターの展開が行われます。そして、コンテキストオブジェクト自体も生成されます。もちろん、クエリー結果のレコードの個数だけⒷのセルが複製されて、その中ではフィールドの内容がタグ要素に合成され、データベースのデータがセルに見えることになります。もちろん、複数のリンクノードを記述して複数のフィールドを指定しても構いません。なお、内部にさらにエンクロージャー/リピーターが見つかれば展開は行いますが、一般には処理速度を考慮すべきところであり、1回のデータベースアクセスで必要なデータを取り出すようにデータベース側を設計しておくことが求められます。
次にテーブル2行目から、行見出しの合成を行います。図5-5-4のように、TBODYタグをエンクロージャーとし、ⒸのセルをTRタグで包んだ要素をリピーターとして通常通りの手順で合成を行います。したがって、Ⓒのセルに含まれるリンクノードのターゲット指定によってコンテキスト名が決まりデータベースアクセスして、取り出されたレコードのフィールドの値が2行目以降の1セル目に合成されます。Ⓒのセルを包むTRタグ要素も、元からページファイルにあったものではなく、単にプログラムで生成したものです。Ⓒのセル内に複数のリンクノードを設けても構いませんし、さらにその中にエンクロージャー/リピーターのセットがあれば展開を進めるのも同様です。
そして、2行目の2列目以降、列見出しのレコード数分Ⓓのセルを付加します。最初は単にセルを付加して、行列をセルで埋める作業を行います。そして、Ⓓのセルを解析してコンテキスト名を求めてコンテキストを決定し、データベースアクセスを行います。クエリー結果のレコードを順番に合成するのではなく、順番に調べて置き場所がテーブル内にある場合、その交差セルに対して合成、つまりリンクノードの指定に応じてフィールドの値をタグ要素内に埋め込む処理を行います。したがって、交差セル用コンテキストで得られた結果でも、対応する行あるいは列がなければ無視されます。この処理はクロステーブルだけで行われており、エンクロージャー/リピーターによる展開とは異なる処理が組み込まれています。したがって、エンクロージャーに相当するノードはないため、図の中ではエンクロージャーに対しては「規定なし」と記載をしました。なお、Ver.5.4-dev現在の実装では、交差セルへの合成は、単に合成するだけなので、同一のセルに2回以上合成すると、それらの数値の文字列がつながって見えるだけです。加算等は実装されていませんので、コンテキストから得られるデータは集計結果になっているものを利用するようにしてください。
最後の交差セルを埋める部分はデータを見ながら説明をしましょう。サンプルファイルはSamples/Practice/crosstable.htmlおよびcrosstable.phpにあります。演習環境を利用している場合には、ブラウザーで「http://localhost:9080」に接続し、そこにある「サンプルプログラム」のリンクをクリックして、サンプルプログラムの一覧を表示します。そこにあるPracticesにある「cross table」の項目をクリックして、動作を確かめることができます。図5-5-6は、そのサンプルを動かした結果です。このサンプルのページファイルは、このセクションで示したページファイルの記述のサンプルと同一のものです。定義ファイルには、item、customer、salesummaryの3つのコンテキストが指定されていて、それぞれ、列見出し、行見出し、交差セルのコンテキストです。
以下、3つのコンテキストの具体的な値をもとに、クロステーブルの動作を検討しましょう。列見出しのコンテキスト「item」と、行見出しのコンテキスト「customer」に関して、クエリー結果のデータを示すと、表5-5-1と表5-5-2のような結果になります。それぞれの値が、セルの中に表示されているのが分かります。
id | name |
---|---|
25 | Onion |
26 | Parsnip |
27 | Peppers |
28 | Potato |
29 | Pumpkin |
30 | Peas |
31 | Rhubarb |
32 | Shallots |
33 | Spinach |
34 | Squash |
35 | Sweet Potato |
id | name |
---|---|
250 | Danio Food, Co. |
251 | Darter Food, Co. |
252 | Dartfish Food, Co. |
253 | Dealfish Food, Co. |
254 | Death Valley pupfish Food, Co. |
255 | Deep sea anglerfish Food, Co. |
256 | Deep sea bonefish Food, Co. |
257 | Deep sea eel Food, Co. |
258 | Deep sea smelt Food, Co. |
259 | Deepwater cardinalfish Food, Co. |
salessummaryクエリーの結果は多数のレコードが取り出されますが、その一部を抜粋したのが表5-5-3です。id=1の最初のレコードですが、item_id=38、customer_id=549です。これらは、列見出しおよび行見出しのコンテキストの中には含まれていませんので、id=1のレコードは無視します。同様に、id=2のレコードも無視します。id=3のレコードは、item_id=30ですので、列見出しのコンテキストから「6列目」であることが分かります。また、customer_id=251であり行見出しのコンテキストから2行目であることが分かります。したがって、交差セルの行列で言えば2行目の6列目、テーブルで数えれば見出しの行が増えているので3行目の6列目に当たるセルに対して、id=3のレコードが展開されることになります。そして、実際に該当するセルに、qtyの値の「8」とtotalの値の「2984」が見えています。
id=4はどうでしょうか? item_id=32なので8列目ですが、customer_id=496に対するレコードが行見出しにはないので、このレコードも無視します。こうして、この後沢山のレコードが無視された後、id=95のレコードが見つかります。item_id=27なので3列目、customer_id=255なので6行目であることが確定するので、対応するセルに対してqtyとtotalの値「41」「12177」が表示されます。salessummaryではこの2つのレコードだけが、クロステーブルの中にあるデータなので、2つのレコードだけが見える結果になったということです。
なお、ここでの突き合わせは、交差セルのコンテキストのrelationキーでのフィールドで決まります。列見出しに対応する設定はforeign-keyは「customer_id」、join-fieldは「id」でした。つまり、列見出しのコンテキストのidフィールドの値と、交差セルのコンテキストのcustomer_idの値を付き合わせるということです。通常、join-fieldは見出しのコンテキストのkeyキーで指定するものかもしれませんが、必ずしもそうでないような場合もあり、その場合に見出しコンテキストでテキストフィールドを設定してフィールドを編集したいこともあるので、見出しコンテキスト側の突き合わせ対象フィールドは、relationキーでの指定から得るようにしました。
id | dt | item_id | customer_id | qty | unitprice | total |
---|---|---|---|---|---|---|
1 | 2010-01-01 00:00:00 | 38 | 549 | 7 | 528 | 3696 |
2 | 2010-01-01 00:04:45 | 24 | 632 | 16 | 781 | 12496 |
3 | 2010-01-01 00:14:10 | 30 | 251 | 8 | 373 | 2984 |
4 | 2010-01-01 00:27:02 | 32 | 496 | 46 | 331 | 15226 |
: | : | : | : | : | : | : |
94 | 2010-01-01 22:02:29 | 16 | 860 | 4 | 472 | 1888 |
95 | 2010-01-01 22:24:34 | 27 | 255 | 41 | 297 | 12177 |
: | : | : | : | : | : | : |
クロステーブルで3つのコンテキストを用意しますが、注意深く設定しなければならないのは、交差セル用コンテキストのrelationキーの値です。列見出しと行見出しとの対応が取れるようなフィールドを指定しなければなりません。また、交差セル用コンテキストは、集計した結果が得られるようにしておく必要があります。ページ合成の中で、合計を取るなどの処理は現在はできません。
このセクションのまとめ
コンテキストを3つ用意することで、それらを列見出し、行見出し、交差セルに展開して、クロステーブルを作成できます。交差セルには、relationキーで指定したフィールドにおいて見出しと対応した値を持つものが配置されます。また、行列の合計を自動的に追加することもできます。
5-6スタイルの設定を自動化するテーマ
見栄えの良いページを作成するには、CSSによるスタイルをかなり細かく設定しなければなりませんが、そのスタイル設定をテーマとしてひとつのフォルダーにまとめておき、それをページあるいはサイト全体に適用する機能をVer.5.6-devで組み込みました。
テーマの機能とdefaultテーマについて
原則としてCSSの定義はアプリケーションごとに作るものであるとも言えるのですが、CSSを適用しないでWebページを作成すると、INTER-Mediatorが生成するようなページネーションやログインパネルがHTMLの定義そのままの形で出てきてしまいます。機能としては実現していても、次のページに移動するボタンがボタンらしく見えていないとユーザーが戸惑う原因になりますし、作っている時も画面がそっけなさ過ぎてモチベーションが落ちてしまいそうです。そこで、テーマの機能を内蔵して、Webページを作った段階で、ある程度のスタイルや画像リソースが自動的に適用されるようにしました。
INTER-Mediatorで自動的に適用されるデザインテーマは、太木裕子氏(京都造形芸術大学キャラクターデザイン学科専任講師)に開発していただきコントリビュートしていただきました。テーマ自体のシステム名称は「default」としていますが、テーマ名として『「楝」OUCHI』という名称がつけられています。「楝色(おうちいろ)」は薄い紫の和色名です。基本となるスタイルであることから、楝=家=HOMEといった言葉の連想に由来しているそうです。テーブルのTHやTD、INPUT等、よく利用するタグについても、見栄えが良くなるようなCSS定義がなされています。
テーマの適用と選択
INTER-Mediatorには、表5-6-1に示すテーマがバンドルされています。defaultは何も指定しないと適用されるものですが、実際にはINTER-Mediatorがページファイルのヘッダー部分に自動的にテーマの定義を取り出してページに適用するlinkタグ要素を付加することで、CSSなどを適用しています。
テーマ名 | 内容 |
---|---|
default | テーマに対する設定がない場合に「楝」テーマが適用される |
least | ページネーションおよびログインパネルのCSSと、処理中を示す表示のためのリソース |
thosedays | 「楝」テーマの機能を組み込む以前のサンプル用CSSファイル「sample.css」を適用した状態と同じにする |
テーマの設定は、以下のいずれかの方法で行えます。IM_Entry関数はもちろん定義ファイルに記述するもので、このテーマの設定は、定義ファイルを参照しているページにだけ適用されます。params.phpファイルに指定すると、そのファイルを参照しているINTER-Mediator全体に適用されるので、例えばサイト全体をまとめて設定したいときに利用できます。
- IM_Entry関数の第2引数(オプション設定)で、themeキーによる値にテーマ名を指定する。
- params.phpファイルに、$themeName変数に対して文字列でテーマ名を指定する。
default以外の付属のテーマについて説明します。leastは、ページネーションとログインパネル以外の設定がない、最小限のテーマです。テーブルや段落等のCSSは全くされていない、文字通り最小限のテーマです。thosedaysは、テーマを実装する以前の状態という意味ですが、そのとき、サンプルファイル用のCSSファイルであるsample.cssをコピーしてアプリケーションに利用していたことに由来します。その時と同じ状態をテーマで実現しています。以前に作成したWebページにdefaultを適用すると、自分自身でCSSの定義を行なっている場合には異なる定義が混在してしまってかえって見栄えには問題が出てくるかもしれません。そこで、最小限ながら以前のsample.cssと同じ状態にするテーマを、既存のWebページ向けに用意してあります。
テーマのカスタマイズ
テーマを自分自身で変更したい場合には、大きくわけて2通りのアプローチがあるでしょう。ひとつは、既存のテーマを使いつつ、特定のページでは、その定義を上書きして変更するというものです。もうひとつは、スタイルそのものを自分で作るかあるいは既存のものを作り変えるという方法です。後者の方法はこの次で説明します。
ページネーションのカスタマイズをしたい場合の例を示しましょう。ページネーションのそれぞれの要素には、表5-6-2のようなスタイルが設定されています。
スタイルシートのセレクタ | ページネーションの該当部分 |
---|---|
#IM_NAVIGATOR | コントローラーの外側 |
.IM_NAV_panel | コントローラー全体 |
span.IM_NAV_info | 文字を表示する部分 |
span.IM_NAV_button | ボタンになる部分(機能するボタン) |
span.IM_NAV_disabled | 機能しないボタンの部分 |
ここでボタンの背景と、黄色いマーキングを別のものに変えたいとします。色のセンスはさておいて、ボタンを白背景の赤文字にしたいとした場合、リスト5-6-1のようにヘッダー部にstyleタグ要素を追加して、ボタンに対するCSSの設定を上書きしてしまいます。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<script type="text/javascript" src="def01.php"></script>
<style>
span.IM_NAV_button {
color: red;
background-color: white;
}
</style>
</head>
<body>
テーマの構造とカスタマイズ
INTER-Mediator既定のテーマは、INTER-Mediatorフォルダーの直下の「themes」フォルダーに収められています。このフォルダー以下は、「テーマ名」、「css」か「images」、実際のファイル、といった構成になっています。cssとimagesは決められたフォルダー名です。通常、cssフォルダーに入れてある拡張子が.cssのファイルは、自動的にマージされて、ページに適用されます。
独自にテーマを作る場合には、defaultフォルダーをコピーして名前を付け直し、その中のファイルを変更するのが一番効率良い方法でしょう。その作成したテーマのフォルダーは、もちろん、INTER-Mediatorフォルダーの直下に配置して構いません。また、可能であれば、コミットしていただければ、テーマのバリエーションが増えるので、開発者としては歓迎です。
しかしながら、INTER-Mediatorフォルダーの中にテーマを入れるとなると、もし、改めてレポジトリからpullしたときに、自分で作ったものが追加されていることでなんらかの対処が必要になるかもしれません。自分だけでテーマを使いたい場合にはINTER-Mediatorフォルダー外にテーマを置きたいと思われるでしょう。その場合、リスト5-6-2のように、params.phpファイルで$altThemePath変数に、テーマが保持されているフォルダーへのパスを文字列で指定します。そのサーバーで認識できる絶対パスを記述してください。また、テーマ名もその場合自分で作ったものになるでしょうから、$themeName変数も記述します。
$altThemePath = "/var/www/thmeme"; //Your original thmeme directory.
$themeName = "blackbird"; //Default theme name.
テーマ内のファイルの内容を取得するには、一般には次の形式で得られます。[ ] の部分は適切な文字列に置き換えますが、css|imagesは、cssないしはimagesのどちらかの文字列が入るということです。以下の形式を、例えば、linkタグ要素のhref属性や、imgタグ要素のsrc属性等に指定して、テーマの中身を取り出すことができます。なお、type=cssの場合にname=を省略することで、cssフォルダーにある全ての.cssファイルの中身をマージして返します。
[定義ファイル名]?theme=[テーマ名]&type=[css|images]&name=[ファイル名]
このセクションのまとめ
テーマの機能によって、特にスタイルシートの設定をしなくても、おおむねデザイン的に整ったサイトを作ることができるようになりました。テーマを自分で定義したり、テーマの定義内容を変更することもできます。
5-7ステップ動作を行うシングルページアプリケーション
モバイルアプリケーションによく見られる「ステップ動作」を実装できる機能をVer.5.7-devで組込みました。ステップ動作は、リスト形式で選択肢が表示され、タップすると次の選択肢が表示されるといった形式のユーザーインターフェースです。また、「戻る」ボタンがあることも特徴です。このひとつひとつの画面をコンテキストで定義し、それぞれの画面をひとつのページファイル内に記述する、シングルページアプリケーション形式でステップ動作のアプリケーションを組み込むことができます。このセクションの内容は、JavaScriptのプログラミングが必須であり、書籍内の順序では後の部分の説明を理解している必要があります。
ステップ動作のサンプルアプリケーション
ステップ動作の機能を理解するために、サンプルアプリケーションを動かしてみましょう。演習環境を利用しているのであれば、トップページにある「サンプルプログラム」のリンクをクリックし、Practicesの表にある、「Mobile」の「step」を、クリックしてください。ステップ動作は、マスター/ディテール形式のユーザーインターフェースと同じ仕組みを使っているため、モバイル対応しています。モバイル端末だとセル全体のどこをタップしても構いませんが、PC動作だと「詳細」ボタンが表示されます。PC/Macで手軽にモバイル端末のシミュレーションをするには、Chromeのデバッガにあるデバイスツールバーを表示する方法が手軽でしょう。デバッガの画面を表示すると、上部に「Elements Console…」とメニューが並びます。その「Elements」のすぐ左側にあるモバイルデバイスのアイコンをクリックしてオンにし、青い色になることを確認してください。そして、画面を更新すると、モバイル端末での表示状態になります。
図5-7-1は、演習環境を稼働させて表示されるサンプルアプリケーションの最初のページです。Chromeでモバイルのシミュレーションを行っています。まずは、東京近辺の都県の名前が並んでいます。あとでコードを確認しますが、TABLEタグを利用しています。そして、ページ上部にはタイトルバー、ページ下部にはINTER-Mediatorを示すバーがあり、残りの部分はテーブルだけで埋め尽くされているのを確認してください。また、サンプルの郵便番号データベースには東京都のデータしかなかったことにお気づきの方は、「神奈川県」などはどういうことかと思われるかもしれませんが、これも後で説明します。なお、次のページに移動して何か表示されるのは、「東京都」だけです。
「東京都」をタップすると、東京都の市区町村名が一覧されます(図5-7-2)。ここで、まず、市区町村のリストもテーブルで構築していますが、スクロールすることを確認してください。その時、タイトルバーとページ下部の表示は画面に固定され、その間でスクロールされるという典型的なモバイルアプリケーションの動作になっていることを確認してください。この動作はINTER-Mediatorとは関係なく、CSSの設定で可能です。これについても、あとで説明します。また、ヘッダーの左側に、◀︎ボタンが表示され、これをタップすると、東京都などの最初のリストが表示されることが分かります。つまり、これは「戻る」ボタンであり、最初の画面では表示されていないことも確認してください。
「市区町村」をタップしたあとは、町域名の一覧が表示されます。この時、前に選択した市区町村に含まれる町域名だけが表示されていることを確認してください。つまり、前のステップの選択肢が、次のステップの一覧表示に対して影響を与えている、つまり検索条件を与えるということができています。そして、最後は、郵便番号、都道府県、市区町村、町域名が一覧され、そこから先にはステップ動作での移動は行いません(図5-7-3)。戻ってまた別の場所を選択することもできます。郵便番号が見える画面では、いずれのセルをタップしても画面遷移は行いませんが、デバッガのコンソールにオブジェクトの内容が出力され、これまでの4つのステップで、何をタップしたのかが記録されていることが分かります。ここで、記録されている状況を例えばデータベースに書き込むなどすることになることも多いかもしれませんが、その作業はJavaScriptでプログラムを記述する必要があります。
ステップ動作のためのコンテキスト定義
定義ファイルの内容を参照しましょう。定義ファイルは、INTER-Mediatorフォルダーの中では、Samples/Sample_Mobile/step_MySQL.phpにあります。ソースコードをWebで参照するにはこちらをクリックしてください。リスト5-7-1は、ちょっと長いですが、IM_Entryの第1引数であるコンテキスト定義部分を示しています。全体的に見て、4つのコンテキストが定義されています。ここで、ステップ動作に特有の設定は、navi-controlキーのstepと、step-hideです。INTER-Mediatorは最初にページ全体の合成を行うとき、step-hideのコンテキストについては、エンクロージャーの要素のdisplayスタイル属性にnoneを設定して、全て非表示にします。また、そのとき、データベースアクセスを行わず、データをリピーターに合成する処理は行いません。一方、stepの方は、通常通り、ページ合成中にデータベースアクセスを行い、リピーターにデータを合成します。つまり、ひとつのstepと複数のstep-hideがnavi-controlに設定されたコンテキスト定義を作ることが、一般的な手法です。
あるステップ動作のコンテキストでタップを行った時に、「次に表示するコンテキスト」をどのように決定するかを説明しましょう。まず、既定の動作をここで説明します。次々と表示されるコンテキストは、定義ファイルの定義の順番になります。つまり、prefectureコンテキストを表示している時にタップすると、次にnavi-controlキーの値がstep-hideのコンテキストを探し、その結果cityコンテキストが選択され、このコンテキストを利用しているページファイルの一部分が更新され、データベース処理が行われてクエリー結果をリピーターに合成します。そして、「次へ」という動作の時にスタック動作を行うグローバル変数に情報を残すので、「戻る」ことも自動的に行えるようになっています。
array(
array(
'name' => 'prefecture',
'table' => 'not_available',
'view' => 'postalcode',
'aggregation-select'=>'MIN(id) AS pref_id, f7 AS pref',
'aggregation-from'=>'postalcode',
'aggregation-group-by'=>'f7',
'records' => 10000,
'maxrecords' => 10000,
'key' => 'pref_id',
'navi-control' => 'step',
'before-move-nextstep'=>'doAfterPrefSelection',
'appending-data'=>array(
array('pref_id'=>101, 'pref'=>'埼玉県'),
array('pref_id'=>102, 'pref'=>'神奈川県'),
array('pref_id'=>103, 'pref'=>'千葉県'),
)
),
array(
'name' => 'city',
'table' => 'not_available',
'view' => 'postalcode',
'aggregation-select'=>'MIN(id) AS city_id, f8 AS city',
'aggregation-from'=>'postalcode',
'aggregation-group-by'=>'f8',
'records' => 10000,
'maxrecords' => 10000,
'key' => 'city_id',
'navi-control' => 'step-hide',
'before-move-nextstep'=>'doAfterCitySelection'
),
array(
'name' => 'town',
'table' => 'not_available',
'view' => 'postalcode',
'aggregation-select'=>'MIN(id) AS town_id, f9 AS town',
'aggregation-from'=>'postalcode',
'aggregation-group-by'=>'f9',
'records' => 10000,
'maxrecords' => 10000,
'key' => 'town_id',
'navi-control' => 'step-hide',
'before-move-nextstep'=>'doAfterTownSelection'
),
array(
'name' => 'wrapup',
'table' => 'not_available',
'view' => 'postalcode',
'records' => 10000,
'maxrecords' => 10000,
'key' => 'id',
'navi-control' => 'step-hide',
'before-move-nextstep'=>'doAfterLastSelection'
),
),
他に、コンテキスト定義ではnavi-titleキーによるタイトルの指定が可能です。ただし、spanなどの要素で、「data-im="_@navi_title"」という属性が指定されたものをページ内のどこかに記述する必要があります。
ページファイルの記述内容
ページファイルは定義ファイルと同じフォルダーにあるstep_MySQL.htmlという名前のフォルダーです。ソースコードをWebで参照するにはこちらをクリックしてください。リスト5-7-2に、定義ファイルで定義した4つのコンテキストを展開する部分を含む、ページファイルのボディ部を示しました。それぞれの展開部分は、特別なものはなく、単にフィールドをセルの値としているだけです。なお、データベース処理については、後でまとめて説明をします。ここで、ステップ動作に必要な準備は、「戻る」ボタンの確保です。リストの最初の方に、class属性が「IM_Button_StepBack」のspanタグがあります。タグの種類はおおむね何でもよく、class属性に必ず前述の名前を指定します。それが、ページ内の見えている場所に配置されていれば構いません。通常はひとつの要素だけで十分と思われますが、複数あってもかまいません。
<div id="header">
<span class="IM_Button_StepBack">◀︎</span>
郵便番号検索
</div>
<div id="content">
<table class="stepbox">
<tbody>
<tr>
<td><span data-im="prefecture@pref"></span></td>
<td class="accessary"></td>
</tr>
</tbody>
</table>
<table class="stepbox">
<tbody>
<tr>
<td><span data-im="city@city"></span></td>
<td class="accessary"></td>
</tr>
</tbody>
</table>
<table class="stepbox">
<tbody>
<tr>
<td><span data-im="town@town"></span></td>
<td class="accessary"></td>
</tr>
</tbody>
</table>
<table class="stepbox">
<tbody>
<tr>
<td><span data-im="wrapup@f3"></span></td>
</tr>
<tr>
<td><span data-im="wrapup@f7"></span></td>
</tr>
<tr>
<td><span data-im="wrapup@f8"></span></td>
</tr>
<tr>
<td><span data-im="wrapup@f9"></span></td>
</tr>
</tbody>
</table>
</div>
画面遷移時に呼び出されるメソッド
これまでの設定で、navi-controlの値がstepおよびstep-hideをもつコンテキストを展開したページの一部分が切り替わり表示できるようになっています。しかしながら、切り替わり前に、何か処理をしたいことが多いでしょう。サンプルでは、例えば、市区町村に「中野区」を選んだ時と、「渋谷区」を選んだ時では、町域名の検索条件が変わります。そうした処理などを組み込むために、コンテキスト定義に、before-move-nextstepキーを定義します。値はメソッド名を示す文字列です。例えば、prefectureコンテキストでは、「'before-move-nextstep'=>'doAfterPrefSelection'」という記述がありますが、これに対して、ページファイルのヘッダー部で、リスト5-7-3のようなメソッドを定義します。メソッドは、INTERMediatorOnPage変数のオブジェクトとして定義します。引数はありません。
INTERMediatorOnPage.doAfterPrefSelection = function () {
var lastSelection = IMLibPageNavigation.getStepLastSelectedRecord()['pref'];
INTERMediator.clearCondition('city');
INTERMediator.addCondition('city', {field: 'f7', operator: '=', value: lastSelection});
};
この例では返り値がありませんが、返り値によって、表5-7-1のように、画面遷移の動作をコントロールできます。したがって、最後の画面で何かの動作を組み込んだり、あるいはコンテキスト定義の順番とは関係のない任意のコンテキストを次に表示するなど、ナビゲーション自体を返り値でコントロールできます。
返り値 | 動作 |
---|---|
false | 画面遷移せず、現在のステップに留まる |
null | コンテキスト定義の順番で決まる次のステップに画面遷移する(returnなしの場合もこれに相当する) |
文字列 | 文字列で指定したnameキーを持つコンテキストに対応した画面に遷移する |
コンテキスト定義内には、表5-7-2に示すキーで、メソッド名を定義できます。いずれも、INTERMediatorOnPage変数のオブジェクトとして、キーに対する値を名前に持つメソッドを定義します。before-move-nextstepキーのメソッドは前に示すように返り値により動作を定義できますが、残り2つのキーに対するメソッドは、引数も返り値も不要です。例えば、あるステップだけ、特定のフッターが必要な場合には、あらかじめdisplayスタイルをnoneにしておいて画面には見えないようにしておき、just-move-thisstepキーの値の名前のメソッドでフッターを表示します。また、just-leave-thisstepキーの値の名前のメソッドでフッターを非表示にします。
コンテキスト定義でのキー | メソッドの呼び出しタイミング |
---|---|
before-move-nextstep | セルをタップしたとき |
just-move-thisstep | コンテキストのページ合成を終えた直後 |
just-leave-thisstep | 次のコンテキストに移行する直前 |
before-move-nextstepキーで指定するメソッド内では、現在のステップや、それ以前のステップで選択した項目など得るために、以下の変数や、メソッドを利用することができます。
IMLibPageNavigation.stepNavigation
ステップ動作で選択したそれぞれのセルが順番に入力された配列。最後の要素が、今表示されている画面での選択結果となる。要素は、key、contextの2つのプロパティを持つオブジェクトである。contextは、そのステップで利用されたコンテキストオブジェクト(IMLibContextクラス)を参照するので、選択したデータはもちろん、関連するフィールドや他のレコードを含めて参照できる。keyは、選択したレコードのキーで、「主キーフィールド名=主キー値」の形式を持つ。コンテキストのstoreプロパティで、keyの値をキーにすると、選択したレコードが取り出せる。
IMLibPageNavigation.getStepLastSelectedRecord()
現在のコンテキストで選択したレコードを得る。
なお、ページの冒頭にスタティックな内容を表示して、ボタンをクリックすればステップ動作が始まるようにしたい場合には、全てのコンテキストのnavi-controlをstep-hideにして、以下のメソッドを実行して、ステップ動作のきっかけを作ることができます。
IMLibPageNavigation.startStep()
コンテキスト定義でのnavi-controlキーの値が全部step-hideの場合、このメソッドを実行することで、コンテキスト定義で最初にnavi-controlキーを持つコンテキストに対応する画面がページ上に表示する。
ステップ動作を自分でコントロールしたい場合には、次のようなメソッドを利用できます。いずれも、ボタンをタップした場合に、特別な処理をするような場合に利用できるでしょう。
IMLibPageNavigation.backToPreviousStep()
「戻る」ボタンと同等な処理を行う。なお、複数ステップを戻るには、このメソッドを必要回数指定する。
IMLibPageNavigation.moveNextStep(key)
セルをタップしたのと同じくステップを進める処理を行う。この時、引数keyが、IMLibPageNavigation.stepNavigationの要素のkeyプロパティに設定される。
例えば、ボタンをタップしたら、別のステップに進みたいとします。ボタンのonclick属性に指定した関数で、「IMLibPageNavigation.moveNextStep("buttontapped");」のように、moveNextStepメソッドを呼び出します。引数は適当な文字列ですが、レコードを選択したときには通常は「主キー=値」の形式になるものの、ここでは確実にレコードを選択したときと異なる値になるようなkeyプロパティを選んであります。そして、before-move-nextstepキーで指定したメソッド内で以下のように、直近の選択結果のkeyプロパティがmoveNextStepメソッドと同じかどうかを判定して、ボタンから次のステップに移動するのか、セルをタップするのかを判別することができます。
var lastKey = IMLibPageNavigation.stepNavigation[IMLibPageNavigation.stepNavigation.length - 1].key;
if (lastKey === 'buttontapped') {
/* ボタンをタップした場合の処理 */
} else {
/* セルをタップした場合の処理 */
}
サンプルでのデータベースアクセス
ここでまず、データベースへのアクセスがどのようになっているのかを説明します。利用するテーブルは他のサンプルでもおなじみの、postalcodeです。日本郵便が配布しているデータで、f3フィールドが郵便番号、f7フィールドが都道府県名、f8フィールドが市区町村名、f9フィールドが町域名を示しています。また、idフィールドに連番が入力されていて、このフィールドが主キーになります。表5-7-3は、4つのコンテキスト定義で指定されている値で合成されるSQLステートメント
コンテキスト名 | 基本のSQLステートメント |
---|---|
prefecture | SELECT MIN(id) AS pref_id, f7 AS pref FROM postalcode GROUP BY f7 |
city | SELECT MIN(id) AS city_id, f8 AS city FROM postalcode GROUP BY f8 |
town | SELECT MIN(id) AS town_id, f9 AS town FROM postalcode GROUP BY f9 |
wrapup | SELECT * FROM postalcode |
まず、最初のprefectureコンテキストでのデータベースアクセス結果を検討しましょう。表5-7-3にあるようにSQLステートメントを実施します。都道府県はf7フィールドで得られますが、単に取ってくるだけだと、大量に「東京都」が出てきてしまいます。ここで、DISTINCTを使ったSELECT文も考えられるのですが、主キーフィールドを設定したいと考えます。この時、f3=東京都のレコードはたくさんありますが、ひとつのidフィールドの値を、コンテキストで得られるリレーションの主キーにするために、f7フィールドが同じレコードをGROUP BYでひとつにまとめるとともに、その中のidフィールドのうち、MIN関数で最小のものを取り出しています。仮にidが1から1000まで全部が東京都のレコードだったとします。その時、id=1なのか、id=2なのか、id=333なのかは、現実にはどれでもいいのです。ただし、f7フィールドしか参照しないというルールが守られていれば、idその中のひとつでいいので、ここでは最小値を取ってきています。prefectureコンテキストでは、appending-dataキーもあるので、結果的には、表5-7-4のようなリレーションが得られます。
pref_id | pref | 注釈 |
---|---|---|
1 | 東京都 | SQL文で得られた結果 |
101 | 埼玉県 | appending-dataキーで追加された結果 |
102 | 神奈川県 | appending-dataキーで追加された結果 |
103 | 千葉県 | appending-dataキーで追加された結果 |
ここで「東京都」をタップしたとします。すると、リスト5-7-4に示したINTERMediatorOnPage.doAfterPrefSelectionメソッドがスタートします。「IMLibPageNavigation.getStepLastSelectedRecord()['pref'];」の実行により、「東京都」のレコードのprefフィールドの値、つまり「東京都」という文字列が得られ、lastSelection変数にセットされています。そして、次のコンテキストcityに対して、「f7 = '東京都'」という検索条件が追加され、つまりは、「SELECT MIN(id) AS city_id, f8 AS city FROM postalcode WHERE f7 = '東京都' GROUP BY f8」というSQLステートメントが実施されます。結果的には、表5-7-5のようなリレーションが得られます。ここでも、同一のf8フィールドでグループ化して、その中の最小のidフィールドの値を利用して、主キーのcity_idを求めています。
city_id | city |
---|---|
1 | 千代田区 |
447 | 中央区 |
605 | 港区 |
cityやtownのコンテキストでセルをタップしたときに呼び出すメソッドは、prefectureコンテキスト同様に、次のコンテキストに検索条件を加えるものです。具体的には、ソースコードを参照してください。
画面に固定されたヘッダーとフッター
まず、モバイルブラウザーでの縮小処理が行われないように、ページファイルのヘッダー部に「<meta name="viewport" content="initial-scale=1"/>」というタグを記述します。必要に応じて、ほかの記述も加えます。
モバイルアプリケーションの特徴である、固定されたヘッダーやフッターは、CSSの機能を利用します。サンプルでは、ページファイル内にスタイルシートを記述しました。ヘッダーとフッターに関連する部分を、リスト5-7-5に示しました。まず、画面全体は、id=containerで囲まれています。その中に、id=headerのヘッダー、スクロールするテーブルのid=contentのブロック、そしてフッターに相当するのはid=IM_CREDITの要素です。container内部はdisplayをflexにして、固定値に配置されるようにしています。id=contentはdivタグで、その中にtableタグのテーブルがあります。そこでスクロールされるように、overflowの値をscrollにしています。
#container {
display: flex;
flex-direction: row;
}
#header {
width: 100%;
text-align: center;
background-color: #2a2780;
color: white;
font-size: 160%;
padding: 8px 0;
}
#content {
overflow: scroll;
}
#IM_CREDIT {
width: 100%;
white-space: nowrap;
}
しかしながら、CSSの定義だけでは画面にきっちりと配置はされません。画面の高さに応じて、id=contentの高さを調整することで、ヘッダーとフッターが画面上下のぴったりとした位置に配置されます。そのために、リスト5-7-6のようなプログラムをページファイル内に記述しました。ページ合成後や、デバイスを回転させた後に関数adjastObjectsを呼び出しています。そして、ヘッダー、フッター、画面の高さから、id=contentの高さを求めて設定をしています。なお、このプログラムは、ページデザインを変更した場合など、ヘッダーやフッターの状況によって作り変えが必要になります。
INTERMediatorOnPage.doAfterConstruct = function () {
document.getElementById('container').style.display = "block";
adjastObjects();
};
window.addEventListener("orientationchange", function () {
adjastObjects();
});
function adjastObjects() {
var headerNode = document.getElementById('header');
var footerNode = document.getElementById('IM_CREDIT');
var wHeight = screen.availHeight;
var stepBoxHeight = wHeight - headerNode.clientHeight - footerNode.clientHeight;
document.getElementById('content').style.height = stepBoxHeight + "px";
}
そのほかのスタイル設定
リスト5-7-7も、ページファイル内に定義したスタイルシートです。まず、stepboxクラスは、tableタグに適用されており、テーブルを画面はばいっぱいに表示するとともに、余計なマージンが設定されないようにして、空白なくヘッダーや画面左右の境界までレイアウトされています。セルについては、高さを32pxにするとともに、フォントサイズを大きめにしました。また、セルの右にある▶︎は、このようにセルを分離してクラスaccessaryを設定し、CSS属性でキャラクタを表示しています。最後には、ヘッダーの左端にある「戻る」ボタンのクラスであるIM_Button_StepBackに対しての設定があります。ヘッダー内で固定位置に配置されるように、positionをabsoluteにして、座標位置や幅を数値で与えています。
.stepbox {
width: 100%;
margin: 0;
}
td.accessary {
width: 20px;
text-align: right;
}
td.accessary::after {
content: "︎▶";
color: #9b9b9b;
}
td {
height: 32px;
font-size: 130%;
}
.IM_Button_StepBack {
position: absolute;
top: 8px;
left: 4px;
width: 40px;
cursor: pointer;
color: #9393ee;
}
演習入力のあるステップ動作のページ
ステップ動作を行うページでは、表示されたものの選択をすることが一般的な使い方ですが、さらに、テキストエリアなどの入力可能なコンポーネントがある場合のサンプルを演習で作ってみます。ここまでに説明したサンプルでは、選択結果はそのままではデータベースにバインドされていないので、もし、データベースに反映させるとしたら、JavaScriptで書き込みを行うようなプログラムが必要でした。同様に入力可能なコンポーネントがあっても直接バインドはできませんので、ローカルコンテキストにバインドして、必要な時にプログラムで入力した値を得られるようにします。なお、この演習は、FileMakerではできません。
定義ファイルエディターを開き最初のコンテキストを入力
2つ目のコンテキストを入力
ページファイルの修正
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1">
<title></title>
<script type="text/javascript" src="def15.php"></script>
<script>
INTERMediatorOnPage.doAfterConstruct = function () {
document.getElementById('container').style.display = "block";
adjastObjects();
};
window.addEventListener("orientationchange", function () {
adjastObjects();
});
function adjastObjects() {
var headerNode = document.getElementById('header');
var footerNode = document.getElementById('IM_CREDIT');
var wHeight = screen.availHeight;
var stepBoxHeight = wHeight - headerNode.clientHeight - footerNode.clientHeight;
document.getElementById('content').style.height = stepBoxHeight + "px";
}
INTERMediatorOnPage.doAfterCitySelection = function () {
var lastSelection = IMLibPageNavigation.getStepLastSelectedRecord()['city_id'];
INTERMediator.clearCondition('opinion');
INTERMediator.addCondition('opinion', {field: 'id', operator: '=', value: lastSelection});
};
INTERMediatorOnPage.doAfterOpinion = function () {
return false;
};
function finishSurvey() {
IMLibQueue.setTask(function(completeTask) {
var opinion = IMLibLocalContext.getValue('opinion');
var selkey = IMLibPageNavigation.stepNavigation[0].key;
var city = IMLibPageNavigation.stepNavigation[0].context.store[selkey]['city'];
alert('市区町村: ' + city +'\nご意見: ' + opinion);
completeTask();
});
}
</script>
<style>
#container {
display: flex;
flex-direction: row;
}
#header {
width: 100%;
text-align: center;
background-color: #2a2780;
color: white;
font-size: 160%;
padding: 8px 0;
}
#content {
overflow: scroll;
}
.stepbox {
width: 100%;
margin: 0;
}
td.accessary {
width: 20px;
text-align: right;
}
td.accessary::after {
content: "︎▶";
color: #9b9b9b;
}
td {
height: 32px;
font-size: 130%;
}
#IM_CREDIT {
width: 100%;
white-space: nowrap;
}
.IM_Button_StepBack {
position: absolute;
top: 8px;
left: 4px;
width: 40px;
cursor: pointer;
color: #9393ee;
}
textarea {
margin: 2px;
padding: 2px;
border: 1px solid gray;
}
button {
margin: 2px;
padding: 2px;
border: 1px solid gray;
}
</style>
<body>
<div id="container" style="display: none">
<div id="header">
<span class="IM_Button_StepBack">◀︎</span>
アンケート
</div>
<div id="content">
<table class="stepbox">
<thead>
<tr><td colspan="2">住んでみたい市区町村を選択してください。</td></tr>
</thead>
<tbody>
<tr>
<td><span data-im="citylist@city"></span></td>
<td class="accessary"></td>
</tr>
</tbody>
</table>
<table class="stepbox">
<tbody>
<tr>
<td>
<span data-im="opinion@f7"></span>
<span data-im="opinion@f8"></span>
を選択しました
</td>
</tr>
<tr>
<td>
この市区町村に関する感想を書いてください<br>
<textarea data-im="_@opinion" style="width: 90%; height: 80px;"></textarea>
</td>
</tr>
<tr>
<td><button onclick="finishSurvey()">アンケート結果を送る</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
Chromeを利用してモバイルシミュレーションで稼働させる
演習のまとめ
- navi-controlキーの値がstepあるいはstep-hideのコンテキストに対応したエリアをそれぞれ1画面として、画面が切り替わるステップ動作のページを作成することができます。ページ内は、データベースの内容を表示することができます。
- クラスがIM_Button_StepBackのspanタグ要素が「戻る」ボタンになります。オブジェクトの配置をするだけで、ボタンが適切に動作します。
- 画面遷移の時に呼び出されるメソッドをbefore-move-nextstepキーで指定します。画面でセルをタップしたときに呼び出され、次の画面のデータベースアクセスの条件設定などができます。
- ページ内にテキストエリアなどを配置した場合、ローカルコンテキストとしてバインドしておけば、後から値をプログラムで取り出すことができます。
このセクションのまとめ
ステップ動作は、ひとつのコンテキストごとに画面に表示する機能で、ある種のモバイルアプリケーションでよく見られる形式であるとも言えます。画面の構築はもちろん、タップ後の画面遷移もコンテキスト定義に定義されている順序で順番に行われます。また、戻るボタンの動作も自動的に行われます。タップ時には定義したメソッドを呼び出せるので、そこでさまざまな処理を記述できますし、遷移をやめたり順序と関係ない別のコンテキストに遷移したり、さまざまな動作を実装できます。ただし、JavaScriptのプログラミングが必要になります。
5-8コピー結果を残すルックアップ
FileMakerのルックアップと同等な機能をINTER-Mediatorでは実装しています。ルックアップは別のコンテキストの値をコピーする機能です。Accessにあるルックアップはポップアップメニューを構築する機能なのでここでのルックアップとは異なります。INTER-Mediatorでは「ルックアップ」と言えば、FileMakerのルックアップ機能を示すものとします。
「ルックアップ」の意味について
まず、ルックアップの動作について概念的に説明をしますが、あまり抽象的だとイメージが湧かないので、表の上でのサンプルで説明をします。図5-8-1のようなテーブルがあったとします。「販売明細」テーブルは、伝票で言えば明細として繰り返して表示されるレコードを管理する部分です。ここで、「販売明細」テーブルとは別に、商品マスターとしての「商品」テーブルがあったとします。
図5-8-1の下半分にあるように、それぞれの「商品ID」フィールドの値を元にテーブル結合すれば「販売明細」の各行で、それまでにはなかった「商品名」と「単価」が得られて、例えば、伝票として実際に表示する場合に商品名が明細内部に見えるようになったり、単価と個数をかけて金額を求めるということができるようになります。
データベースの正規化理論では当たり前のことです。このようなマスターを別テーブルに用意すると、例えば、単価が変われば、マスターだけを変更すると、それを参照している明細全てて単価が変更できます。ただ、これはそのようにしたい場合はそれでいいのかもしれませんが、逆に、単価が変わっても、すでに発行した伝票の明細の単価は変わってほしくない場合もあります。どちらが正しいということではなく、これはそのアプリケーションに必要な要求がどちらかということです。前者のような状況では、正規化したテーブルを用意すればいいのですが、後者のような場合どうすれば良いかはなかなか難しく、よくある方法は、単価が期間で決定される場合には「商品」テーブルにフィールドを増やして「単価の有効期間」的な概念を導入して、うまく処理をする必要があります。ここではその話はメインではありませんので、できるけども難しいというところで話を終わらせます。
このような「その時点での単価を記録したい」というニーズに対応するのがルックアップです。図5-8-2を参照してください。ここでは、「販売明細」に「商品名」「単価」というフィールドがありましたが、図5-8-1では空欄のままでした。これらのフィールドに、「商品」テーブルの現在の値をコピーします。その時、「商品ID」がコピーする元データを取り出す手掛かりになります。もちろん、「販売明細」の「商品ID」を元に、同一の値を持つ「商品」テーブルのレコードを探して、該当するフィールドをコピーするということを行います。
この仕組みを一般的に利用できるようにしたものが、「ルックアップ」です。ここでは説明のために、2つのレコードを同時に更新しましたが、INTER-Mediatorでは基本的にはユーザインターフェースのレイヤでコピー作業を行い、ここで、外部キー(「商品ID」フィールド)をきっかけにするために、1レコード単位でルックアップ動作をするようになっています。FileMakerはメニュー選択等により、複数のレコードで処理ができるようになっていますが、INTER-Mediatorは1レコード単位での動作が基本です。
このような動作について、正規化に反するのではないかという意見もあるかもしれませんが、「単価は一定ではない」という条件がシステムに入り込むとしたら、単純な商品マスターは要求を取り込めていないということになります。「一定ではない」ものをいかにうまく扱うかがアプリケーション開発者の腕の見せ所です。正規化の理論に完全にマッチする回答ではないかもしれませんが、このルックアップは意外に色々な状況にうまく適合してくれます。一般に「紙の上での作業」は、基本、ルックアップと同じです。内容は紙の上に固定されるので、マスターの変更に追随しては困ることが多いでしょう。また、コピーしたフィールド自体を変更しても問題ない場合は、例えば、単価の値引きといった作業も手軽にできます。完全を目指すよりも、ルックアップのような仕組みをうまく利用することの方が、利用者にとって都合が良く、開発者はシンプルな手法で対応できるということは、FileMakerでの長年の開発経験からも証明されています。
サンプルでの動作をまずはチェックする
サンプルの中にルックアップを実装したものがあるので、それを見てみましょう。サンプルのページの「Any Kinds of Samples」の中にある「Client-Side Calculation Page」のMySQL対応版を見てみます。レポジトリ内では、samples/Sample_invoice/invoice_MySQL.htmlとinclude_MySQL.phpが対象ファイルです。このサンプルは、伝票形式なのですが、ルックアップも利用できるように、デザイン的にはちょっと変な感じになっています。明細がないページを作り、ここで明細を新たに作ったすぐの結果は、図5-8-3のとおりです。
明細の中は、明細の1行を管理するitemテーブルを元にしたitemコンテキストと、商品マスターであるproductテーブルを元にした、productおよびproduct_listコンテキストがあります。productコンテキストはルックアップを実施するために追加で必要なコンテキストです。product_listはポップアップメニューの選択肢のために利用します。明細の最初のセルを理解するのに必要な部分をリスト5-8-1に示しました。
itemテーブルのフィールド(抜粋)
{id, invoice_id, product_id, qty, product_unitprice, product_name, product_taxrate}
productテーブルのフィールド(抜粋)
{id, category_id, unitprice, name}
リストの最初のセルのHTML
<div>
<input type="text" data-im="item@product_id" size="2">
</div>
<div class="inline" data-im-control="enclosure">
<div class="inline" data-im-control="repeater">
<span data-im="product@name"></span>
</div>
</div>
<select data-im="item@product_id" class="_im_test-product-id">
<option data-im="productlist@id@value productlist@name"></option>
</select>
リストの最初のセルには、データベースとバインドした要素が3つあります。最初が、item@product_idとバインドしたテキストフィールド、3つ目に同じフィールドであるitem@product_idとバインドしたポップアップメニューがあります。2つ目は、data-im-control="enclosure"がある要素に囲まれており、この中にはproductコンテキストのnameフィールドが表示されています。productコンテキストのrelationキーの値は、[['foreign-key' => 'id', 'join-field' => 'product_id', 'operator' => '=',]] となっていて、itemコンテキストのproduct_idと、productコンテキストのidで照合するリレーションシップが定義されています。そのため、ターゲット指定がproduct@nameの要素には、ポップアップあるいはテキストフィールドで指定したproductレコードのnameフィールド(つまりは商品名)が見えているはずです。この2つ目のコンテキストは、リレーションシップの先の値を実際に表示して確認するために用意しており、一般にはアプリケーションでこうした措置は不要でしょう。
ここでポップアップメニューで「Orange」を選択します(図5-8-4)すると、ポップアップメニューが変わるのは当然として、product_idフィールドが変わったので、最初のセルの最初のテキストフィールドの値も変わります。そして、product_nameにはOrange、unitpriceには1540と、productテーブルのid=2のレコードの値が入っています。
ここでは、3列目と4列目の2つのセルがリスト5-8-2のように存在し、いずれも、itemコンテキストのproduct_name、product_unitpriceのフィールドにバインドされています。これに加えて、data-im-control属性が存在し、3列目の指定は、itemコンテキストのproduct_idの値が変更されたら、productコンテキストのnameフィールドの値を取り出して、このテキストフィールドに入れるということを意味しています。結果的に、itemコンテキストのprodcut_itemフィールドに、product_idフィールドに対応したproductテーブルのnameフィールドの値がコピーされます。4列目もフィールド違いますが、基本的には同じ動作になります。
product_name列のセルにあるテキストフィールド
<div>
<input type="text" size="20"
data-im="item@product_name"
data-im-control="lookup:item@product_id:product@name">
</div>
unitprice列のセルにあるテキストフィールド
<div>
<input class="price" type="text" size="8"
data-im="item@product_unitprice"
data-im-control="lookup:item@product_id:product@unitprice"
data-im-format="number()"
data-im-format-options="useseparator">
</div>
ここで、product_nameの列のフィールドは、itemコンテキストのproduct_nameフィールドであり、productテーブルのnameフィールドではありません。そこで、商品名を適当に変えても、1列目に見えている商品名(これはproductテーブルのnameフィールドが見えている)は「Orange」のままになります。つまり、「Orange」という文字列がproduct_nameフィールドにコピーされていて、それを修正しているので、マスター側には変更の影響は及ばないということになります。
ルックアップを実行するための設定
ルックアップを設定するためには、data-im-control属性の設定が必要ですが、それに至るまでに、コピー先のフィールドの用意、リレーションシップ先のコンテキスト定義など、さまざまな作業が発生します。自分自身のバインドとも関係あるため設定はややこしいですが、可能な限りシンプルに設定できいるように考えました。
data-im-control属性は次のようなルールで記述します。まず、最初に「lookup」という決められたキーワードを記述し、続いて、半角のコロン(:)で区切って、lookup意外にさらに2つの記述を行います。lookupを記述するのは第一パートと呼ぶことにします。第二パートは、変更が発生するフィールドを、「コンテキスト名@フィールド名」つまりターゲット指定の形式で指定します。第二パートで指定したフィールドが変更されると、第三パートで指定した「コンテキスト名@フィールド名」のフィールドの値を、自分自身の要素にバインドしているフィールドにコピーします。第3パートに出てくるコンテキストは、原則として第二パートのコンテキストとの関連付けが成り立つようなrelationキーの定義が必要になります。第2、第3パートは、フィールド名以降は何も指定しないでください。ターゲット指定と書きましたが、実際にはコンテキストに含まれるデータベースから取り出した値になり、要素に関連する指定ではないからです。
このセクションのまとめ
リレーションシップの先からデータをコピーしてフィールドに設定するルックアップの仕組みは、INTER-Mediatorではユーザインターフェース上で実現しています。ひとつのレコードに対して、該当するフィールドが更新されると、別のフィールドが別のコンテキストから取り出した値で埋められるという動作になります。ルックアップの仕組みは意外に便利に使える場面があるので、データベース設計では考慮する必要がある仕組みです。
5-9アプリケーションのローカライズ
ブラウザの有線言語に応じて、ページ上のメッセージを切り替える仕組みをここでは「ローカライズ」と称します。見出しや表のラベルなど、常に一定で良い文字列もありますが、場合によってはデータベースの出力結果を言語ごとに文字列を切り替えるということをやりたいかもしれません。これらの仕組みを紹介します。
ローカライズが可能な機能
ローカライズの機能は、ひとつではなく、いくつかの機能で実現しています。また、「新規レコード」ボタンの名前を置き換えるカスタマイズの機能(コンテキスト定義に記述するbutton-namesキー)も、ある意味で、状況に応じた文字列の置き換えではありますが、ここでは、ブラウザの言語に応答する機能に絞ることにします。次のような機能があります。それぞれ、順番に説明をします。
- ①システムが生成する文字列のローカライズ
- ②特定のHTML要素内の文字列
- ③データベースから取り出した文字列
システムが生成する文字列とそのローカライズ
「挿入」や「削除」のボタンがありますし、場合によってはアラートボックスで何かメッセージが出てきます。それら、システムが利用する文字列(以下、「システムメッセージ」と呼びます)は、PHPのソースコードの中に定義してあります。レポジトリでは、src/php/Message/MessageStrings.phpに英語の文字列を定義し、これを基準、つまり言語に対応するリソースが用意されていない場合に選択される言語(もしくは文字列)とします。このクラス以外に、日本語の文字列として、src/php/Message/MessageStrings_ja.phpが定義されています。残念ながら、Ver.12現在他の言語についてはリソースは用意されていません。リソース自体は、日本語リソースのクラスと同様、MessageStrings_言語.phpのファイルとクラスを用意して、配列内にあるメッセージを置き換えれば作成はそれほど難しくはありません。
このメッセージを変更するには、もちろん、ソースを修正すれば置き換えられますが、手軽な方法ではなく、また、変更結果を後々管理するとすれば、その方法を取ることは躊躇しそうです。そこで、params.phpファイルの配列に定義することで、値をそのアプリケーション全体に反映させることが可能です。
params.phpファイルに設定するには、リスト5-9-1のように、$messages配列を定義します。1次元目は言語で、英語は'default'にします。英語以外は日本語なら'ja'など、クラス名の最後の2文字を指定します。2次元目はメッセージ番号です。この番号は、ソースコードの中を調べて該当する番号を記述してください。以下の定義により1022番のメッセージ(非対応のブラウザを使っている場合のメッセージ)を代入した文字列に置き換えることができます。
$messages['default'][1022] = "We don't support Internet Explorer. We'd like you to access by Edge or any other major browsers.";
$messages['ja'][1022] = "Internet Explorerは使用できません。Edgeあるいは他の一般的なブラウザをご利用ください。";
特定のページ内要素をローカライズする
ページファイル内でローカライズしたい要素に対して、data-im-locale属性を指定し、その項目に対する文字列をparams.phpあるいはIM_Entry関数の第2引数(オプション引数)の配列で指定します。リスト5-9-2には、thタグとh1タグに、data-im-locale属性が指定してあります。そして、このページファイルから、リスト5-9-3のような定義ファイルを参照しているとします。thタグのdata-im-localeの値は「category」です。もし、英語が優先言語のブラウザでページを参照したときには、termsキー以下、英語を示すen以下の"category"キーの値を取り出し、thタグの中身は「Category」となります。日本語が優先言語なら同様にja以下を探して「カテゴリ」がthタグの値に設定されます。もし、英語と日本語以外のブラウザを利用してページを表示した場合は、もともとthタグに設定されている「category」が見えます。したがって、言語ごとの文字列を定義するだけでなく、ページ上にも既定の文字列をきちんと入力しておく必要があります。h1タグのdata-im-locale属性は「page|title」となっています。これは、terms/言語以下の部分を、|で区切って階層的に辿ることができ、ここではpageキーの下のtitleキーの値を取り出します。配列をフラットに定義するだけでなく、分類ができるようになっていると考えてください。
<h1 data-im-locale="page|title">Contact Management</h1>
<table>
<tr><th data-im-locale="category">category</th>....
IM_Entry(
[...], // コンテキスト定義
[
"terms" => [
"en" => [
"header" => "INTER-Mediator - Sample - Form Style/MySQL",
"category" =>"Category",
"check" => "Check",
"page" => [
"title" => "Contact Management (Sample for Several Fundamental Features)",
],
],
"ja" => [
"header" => "INTER-Mediator - サンプル - フォーム形式/MySQL",
"category" =>"カテゴリ",
"check" => "チェック",
"page" => [
"title" => "コンタクト先管理 (さまざまな機能を確認するためのサンプル)",
],
],
],
],
["db-class" => "PDO",],
0
);
前の例ではバインドしていない要素に対してローカライズを行いましたが、バインドしている要素に対しても行えます。リスト5-9-4は、ポップアップメニューの選択肢に関してローカライズを施します。optionタグに対してdata-im-locale属性が指定されていて、その値「way」に対する配列が、リスト5-9-5に示す定義ファイルのオプション引数に指定があります。ここで、wayの配列について、キーにはデータベースから得られる値、そしてその値には実際に選択肢として見える文字列を指定します。つまり、データベースからは「Calling」や「Mail」という値を得て、書き込みもこれらの文字列になります。そして、ブラウザが英語の場合にはそれぞれ「Telephone」「Papar Mail」、日本語だと「電話」「電子メール」という文字列に置き代わります。ここでのway以下の配列のキーにない値が得られら場合は、その値そのものが利用され、つまりはローカライズ対象外となります。
<select data-im="contact@kind">
<option data-im="cor_way_kindname@kind_id@value cor_way_kindname@name_kind"
data-im-locale="way"></option>
</select>
IM_Entry(
[...], // コンテキスト定義
[
"terms" => [
"en" => [
"way" => [
"Calling" => "Telephone",
"Mail" => "Paper Mail",
"Email" => "Electronic Mail",
],
],
"ja" => [
"way" => [
"Calling" => "電話",
"Mail" => "手紙",
"Email" => "電子メール",
],
],
],
],
["db-class" => "PDO",],
0
);
データベースとバインドした要素のローカライズについては、読み出し時のみに適用されます。前の例では、optionのvalue属性はデータベースに存在する値、optionタグの値は置き換えた値になって、つまりは書き込むときにはデータベースにあるべき値(つまり、wayキーの配列のキーにあるいずれかの値)になります。したがって、ローカライズの結果とポップアップメニューを選択したときにデータベースに書き込む値はうまく対応づけられます。一方、同じことをテキストフィールドに設定した場合、テキストフィールドには言語ごとの値に置き換わったとしても、テキストフィールドを修正してデータベースに書き戻す際にはテキストフィールド値そのものがデータベースに書き込まれます。その点では、このバインドした要素のローカライズは、ポップアップ、チェックボックス、ラジオボタンでの利用が中心であると考えられます。
以上のように、「data-im-localeがあって、data-imがない」場合と、「data-im-localeとdata-imがある」場合とに分けられます。前者は、data-im-locale属性の値を元にterms/言語以下の値に置き換えますが、後者はさらにデータベースから取り出した値をterms/言語以下の配列に適用して文字列変換を行います。前の例では、termsキーを定義ファイルに設定しましたが、リスト5-9-6のようにparams.phpでは$terms変数に定義して、システム全体に同一の変換テーブルを与えることもできます。
$terms = [
'en' => [
'header' => 'INTER-Mediator - Sample - Form Style/MySQL',
'page-title' => 'Contact Management (Sample for Several Fundamental Features)',
],
'ja' => [
'header' => 'INTER-Mediator - サンプル - フォーム形式/MySQL',
'page-title' => 'コンタクト先管理 (さまざまな機能を確認するためのサンプル)',
'category' => 'カテゴリ',
'check' => 'チェック',
],
];
このセクションのまとめ
ページ内の要素の文字列を接続してきた言語ごとに異なるものにすることができます。つまり、これによって、ブラウザの言語に応じて、異なる文字列をページ上に表示することができ、いわゆるローカライズの仕組みが利用できます。要素そのものの値を言語ごとに切り替えるだけでなく、データベースから取り出した値のローカライズも可能です。