Chapter 5
さまざまなユーザーインターフェース構築

この章は、INTER-Mediator Ver.10をもとに記載しました。

このセクションでは、定義ファイルやページファイルの設定だけで可能な機能のうち、これまでに説明していない機能について説明をします。最初は一覧と詳細を行き来するユーザーインターフェース、続いて電子メールの送信、異なるクライアント間で編集結果を連動させる方法、JavaScriptで作られたユーザーインターフェース部品を利用する方法を説明します。

5-1マスター/ディテール形式のナビゲーション

一覧と詳細を切り替えて表示するようなユーザーインターフェースはよくみられます。業務系システムでは多くの場合、こうした動作が基本です。INTER-Mediatorではこうした動作を2つのコンテキストで実現して自動的に切り替えるユーザーインターフェースを用意します。そこまでの作業は、特別なプログラムコードを書かなくても、定義ファイルとページファイルの設定だけで可能です。また、一覧と詳細の切り替え時にプログラムを記述すれば、より高度なユーザーインターフェース構築も可能です。

マスター/ディテールあるいは一覧/詳細

 データを一覧表示し、さらに特定のレコードについてより多くの情報を表示するといった形式のユーザーインターフェースは一般的なテクニックです。その場合、異なる画面であるだけに、2つのページを作成しておき、それぞれの機能動作を組み込むということが一般的かもしれません。しかしながら、INTER-Mediatorでは、ひとつのページに一覧表示のためのコンテキストと、詳細表示のためのコンテキストを両方を用意して、切り替えることなどが可能です。

 iOSには、UISplitViewControllerというクラスがあり、iPadの「メール」などにみられるように、一覧と詳細を同時に、あるいは個別に表示できる仕組みがあります(図5-1-1)。INTER-Mediatorの機能は、このスプリットビューをヒントにしています。

図5-1-1 UISplitViewControllerを利用したアプリケーションの例

 FileMakerを利用するときに、レイアウトを2種類作成し、同じTOを2つのレイアウトに割り当てることで、一覧(図5-1-2)と詳細表示(図5-1-3)の切り替えがスムーズに行われます。切り替えるために、なんらかのスクリプトは必要ですが、一覧で選択した結果は、TOで記録されるので、レイアウトを切り替えるだけで、一覧で選択されているレコードを詳細レイアウトでも表示することができます。

図5-1-2 FileMakerでの一覧表示
図5-1-3 FileMakerでの詳細表示

 これらのユーザーインターフェースを本コースでは、「マスター/ディテール形式」あるいは「一覧詳細形式」と総称することにして、「一覧表示側」「詳細表示側」という用語で、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-hidedetail一覧と詳細が切り替わる(detail-topと同じ)
master-hidedetail-top一覧と詳細が切り替わる。一覧に戻るボタンは詳細の上部
master-hidedetail-bottom一覧と詳細が切り替わる。一覧に戻るボタンは詳細の下部
master-hidedetail-update一覧と詳細が切り替わり、詳細から一覧に戻る時に一覧が更新される
masterdetail一覧と詳細が同時に表示される
表5-1-1 navi-controlキーに設定可能な値

 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タグ要素
表5-1-2 一覧詳細形式の表示により自動的に付加されるclass属性値

一覧と詳細の切り替え時に呼び出されるメソッド

 一覧表示と詳細表示で、コンテキスト内のリンクノードについては、データベースの内容がそれぞれ表示されますが、それ以外のなんらかの処理を追加したい場合には、いくつかのメソッドを利用することができます。少ない作業で確認ができるので、このセクションの演習で実際にプログラムを追加して動作を紹介しておきます。

演習一覧と詳細を利用したユーザーインターフェース

 iPadのようなユーザーインターフェースや、一覧と詳細が切り替わるユーザーインターフェースを実際に作成してみましょう。また、JavaScriptを利用した高度なカスタマイズも紹介します。

2つのコンテキストを定義ファイルに定義

1ここからの作業は、Webブラウザー上で行います。まず、演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def11.phpを編集する」をクリックし、定義ファイルエディターでdef11.phpファイルを編集します。(もし、他の用途で11番目を利用しているのなら、例えば、def21.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6nameに「person_list」、keyを「id」、pagingを「true」、repeat-controlを「confirm-insert」、recordsを「10」、maxrecordsを「100」とします。
[MySQL]の場合
viewとtableは「person」とします。
[FileMaker]の場合
viewとtableは「person_layout」とします。
Contextsのその他のテキストフィールドは空白にします。
7Contextsという見出しのすぐ下の「追加」ボタンをクリックします。コンテキストの定義領域がひとつ分増えます。
8nameに「person_detail」、keyを「id」、recordsを「1」、maxrecordsを「1」とします。
[MySQL]の場合
viewとtableは「person」とします。
[FileMaker]の場合
viewとtableは「person_layout」とします。
Contextsのその他のテキストフィールドは空白にします。
9Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
10Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

ページファイルの作成と表示

1「http://localhost:9080」で開いたページに戻り「page11.htmlを編集する」をクリックし、ページファイルのpage11.htmlを編集するページファイルエディターが開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
2「http://localhost:9080」で開いたページに戻り、「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。2つのテーブルが見えています。ひとつは、personテーブルの内容が一覧になっています。もうひとつはpersonテーブルのひとつのレコードが見えています。ページネーションが見えていますが、こちらは定義ファイルで指定した通り、テーブルの一覧表示のコンテキストに関連付けられたものです。ここまでは、本コースでこれまでにやってきたことと同一です。
3「page11.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを編集する」をクリックして開きます。そして、2つのTABLEタグを囲むようにDIVタグを定義して、スタイル属性を設定します。displayスタイル属性をflexにすることで、テーブルが2つ横に並ぶようになります。gapによって、テーブル間に適当な空白を入れます。
<body>
  <div id="IM_NAVIGATOR"></div>
  <div style="display: flex; gap: 1em">
  <table>
    :
  </table>
  <table>
    :
  </table>
</div>
</body>
4「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。style属性で指定した通り、テーブルが左右に配置され、テーブル間には空間が作られています。

同一ページでのマスター/ディテール形式のユーザーインターフェース

1「def11.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def11.phpを編集する」をクリックして開きます。
2最初の「person_list」コンテキストのnavi-controlを「master」にします。
32つ目の「person_detail」コンテキストのnav-controlを「detail」にします。設定後、Tabキーを押すなどして、入力結果を確定させてください。
4「page11.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを編集する」をクリックして開きます。一覧を表示するテーブルの行の最初に、空のセルを付け加えておきます。
<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>
5「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。一覧のテーブルの各行に追加した空白のセルの中に、「詳細」ボタンが自動的に設定されています。
64番目の「詳細」ボタンをクリックすると、右側には、一覧表示側と対応したレコードの内容が表示されています。つまり、「詳細」ボタンをクリックしたレコードの内容が右側のテーブル(詳細表示)に表示しています。iPadでよく見られるような一覧と詳細が左右に並ぶ形式のユーザーインターフェースがこれで実現しています。
7詳細表示側はテキストフィールドになっています。nameフィールドの中身を変更してみて、ReturnキーあるいはTabキーを押して設定を確定します。
8自動的に左側の一覧表示側のnameフィールド値も、書き換えたものに即座に変更されました。同一のレコードの同一のフィールドは、ページ内では連動しています。

一覧表示側に対して作用するページネーション

1一覧表示側で、10より多くのレコードがあるようにします。ない場合には、「レコード追加:person_list」ボタンをクリックしてレコードを追加します。このボタンが見えない場合には、「更新」ボタンをクリックして、ページを更新してください。「レコードを本当に作成していいですか?」とたずねられるので、OKボタンをクリックします。「レコード追加」ボタンがない場合には、ブラウザーの更新機能を利用するか、ページネーションの「更新」ボタンをクリックして、ページを更新します。
2例えばレコードを全部で13個作成した場合、最初の10レコードが左側の一覧表示側に見えています。また、ページネーションコントロールでは、次のページに移動するボタンがクリックできるようになっています。
3ページネーションコントローラーの「>」ボタンをクリックして、ページネーションを次のページに移動します。左側の一覧表示側は11レコード目より10レコード以内のレコードが一覧されていますが、詳細表示側は見えているリストの最初のレコード(idフィールドが「12」)が見えています。
4最後のレコードの「詳細」ボタンをクリックして、詳細表示側で適当に入力しました。もちろん、その結果はデータベースに保存されるとともに、一覧表示側にも即座に反映しています。

レコード削除に対する詳細側の動作

1「def11.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def11.phpを編集する」をクリックして開きます。
2最初の「person_list」コンテキストのrepeat-controlを「confirm-insert confirm-delete」にします。
3「page11.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを編集する」をクリックして開きます。一覧を表示するテーブルの行の最後に、空のセルを付け加えておきます。
  <table>
    <tr>
      <td>
        <div data-im="person_list@name"></div>
        <div data-im="person_list@mail"></div>
      </td>
      <td></td>
    </tr>
  </table>
4「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。一覧のテーブルの各行に、「削除」ボタンが自動的に設定されています。
5適当なレコードの「詳細」ボタンをクリックして、詳細表示側に何か表示されている状態にします。
6詳細表示側に表示されている対応するレコードを一覧表示側で特定して、そのレコードの「削除」ボタンをクリックします。削除するかどうかをたずねるので、OKボタンをクリックして、本当に削除します。
7一覧表示側からレコードは消えました。

一覧と詳細が切り替わるユーザーインターフェース

1「def11.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def11.phpを編集する」をクリックして開きます。
2最初の「person_list」コンテキストのnavi-controlを「master-hide」にします。Tabキーを押して、確実に内容を定義ファイルに反映させるようにしてください。
3この状態で、Webアプリケーションの表示がどのようになっているか確認してみましょう。「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。すると、一覧表示部分だけが見えています。
4適当に「詳細」ボタンをクリックすると、そのクリックしたページの詳細表示側だけが見えています。つまり、一覧と詳細が切り替わるユーザーインターフェースが自動的に構築されています。詳細側では、ページネーションも見えていないことを確認してください。
5詳細側の「一覧表示」ボタンをクリックすると、一覧表示側つまり、最初の状態に戻ります。ページネーションも見えるようになっています。
6「def11.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def11.phpを編集する」をクリックして開きます。
72つ目の「person_detail」コンテキストのnav-controlを「detail-bottom」にします。設定後、Tabキーを押すなどして、入力結果を確定させてください。
8「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。一覧表示側だけが表示されています。適当なレコードの「詳細」ボタンをクリックして詳細表示側を見てみます。「一覧表示」ボタンが、下部に表示されました。

エンクロージャー外の要素のコントロール

1「page11.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを編集する」をクリックして開きます。ヘッダー部にスクリプトを追加するとともに、ボディ部の最初に一覧表示と詳細表示の両方の見出しを表示しておきます。そして、詳細側はdisplay属性をnoneにして、非表示にしておきます。スクリプトは、一覧から詳細あるいはその逆に変化する段階で呼び出されるメソッドで、H1タグの見出しをそれぞれ表示/非表示を切り替えているだけです。
<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>
使用しているメソッドについての説明は、この演習のすぐ後にある『一覧と詳細の切り替え時に呼び出されるメソッド』を参照してください。
2「page11.htmlを表示する」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新機能を使ってページ内容を更新します。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page11.htmlを表示する」をクリックして開きます。一覧表示側だけが表示されていて、ページの見出しは「住所録一覧表示」だけが表示されています。「住所録詳細表示」は初期状態ではdisplayスタイル属性が「none」なので、非表示です。
3適当な「詳細」ボタンをクリックして、詳細表示にすると、見出しは「住所録詳細表示」に切り替わります。これは、一覧から詳細に移行する途中でINTERMediatorOnPage.naviAfterMoveToDetailメソッドが呼び出され、ヘッダーに記述したプログラムが実行され、一方の見出しは非表示に、もう一方は表示されるようになったということです。
4「一覧」ボタンをクリックして、一覧表示にすると、見出しは「住所録一覧表示」に戻ります。これは、詳細から一覧に移行する途中でINTERMediatorOnPage.naviAfterMoveToMasterメソッドが呼び出され、ヘッダーに記述したプログラムが実行され、一方の見出しは非表示に、もう一方は表示されるようになったということです。

演習のまとめ

一覧と詳細の切り替え時に呼び出されるメソッド

 以下、マスター/ディテール形式のページにおいて使用可能な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-3 moveToDetailメソッドに指定する引数

 このメソッドの返り値は、関数です。その関数を引数なしで呼び出すことで、詳細への画面切り替えが発生します。例えば、マスター領域の最初のレコードに対応した詳細を表示するには、リスト5-1-1のようなプログラムで可能です。

リスト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メソッドで、プロパティ名の配列を得て、その最初の要素から、主キーフィールド名とその値を分離して得ています。

リスト5-1-2 ページを表示したときに詳細を表示する
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新たなレコードを作るアクションを起こした後にメールを送信する
表5-2-1 send-mailキー(messagingキー)の連想配列に設定可能なキー

 設定の上で若干柔軟性が低いと思われるかもしれません。例えば、フィールド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送信先が含まれるフィールド名
ccCc先が含まれるフィールド名
bccBcc先が含まれるフィールド名
subject件名が含まれるフィールド名
bodyメール本文が含まれるフィールド名
template-context本文のテンプレートとなるコンテキストとレコードを指定
storeメール送信の記録を行うコンテキストを指定
attachmentメールにファイル添付を行う場合
f-optionUNIXでSMTPサーバーを経由しない場合にtrueを指定すると、fromの指定が有効
body-wrap右端の折り返しのバイト数(指定がないと72バイト)。0だと折り返ししない
表5-2-2 send-mailキーの連想配列の値に設定する連想配列に設定可能なキー

 メールの作成方法は、この後に演習を通じていくつかの事例を示しながら解説をします。宛先や送信者、本文は、対応するキーに対する値がそのまま出力されますが、コンテキストのレコードにあるフィールドの値をそこに入れ込むには「@@@フィールド名@@@」のような記述を使います。その部分がフィールドの値に置き換わってメール送信されます。また、メールの元データをデータベースに入れておき、それを利用することも可能です。

 表の中のf-optionは、UNIXマシンのsendmailコマンドを使ってメールを送るときに、送信者を指定したのにもかかわらず、送信者が、www(あるいはApacheの稼働ユーザー)になってしまうようなときに指定をしてください。OSに組み込まれているメール送信コマンド等の動作に依存しますが、より確実に送信者の指定ができるはずです。

 body-wrapは、長い行の折り返しを、Shift-JISのバイト数で指定します。なお、折り返しを入れますが、比較的追い込みを積極的に行うアルゴリズムを組み込んであります。

演習Post Onlyモードのページでメールを送信する

 Post Onlyモードでは、アンケートなどで使われることが多いと思われます。そのとき、投稿した上で、確認のメールを出すということはよく行われます。そうした場面を想定して、3種類のメールの送信方法を紹介します。

演習環境にメールサーバのコンテナを追加する

1演習環境を作成したレポジトリ「IMApp_Trial」を特定して、ルートにあるdocker-compose.yamlファイル開きます。
2ファイルの最後の5行の部分を特定します。mailhog: 以降の行が#によってコメントになっていますが、全てのコメントを外します。そして、ファイルを保存します。
:
    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"
3ターミナルを開き、レポジトリのルートをカレントディレクトリとして、以下のコマンド、つまりコンテナの構築コマンドを実行します。
docker-compose up -d
しばらく待つと、mailhogという名前のコンテナが新たに稼働します。このコンテナは、メールサーバの役割を持つコンテナですが、SMTPのリクエストを受け付けて、それをWebページで表示する機能を持ちます。実際にメールをインターネットに対して飛ばすことはないので、デバッグ等で安心して利用できます。

コンテキストを定義ファイルに定義

1ここからの作業は、Webブラウザー上で行います。ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def12.phpを編集する」をクリックし、定義ファイルエディターでdef12.phpファイルを編集します。(もし、他の用途で12番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6name、table、viewに「survey」、keyを「id」とします。このコンテキストには他にテキストフィールドがありますが、すべて空白にします。
Contextsのその他のテキストフィールドは空白にします。
7Contextsのすぐ下にある「追加」ボタンをクリックして、コンテキスト定義をひとつ増やします。
8nameに「survey_list」、table、viewに「survey」、keyを「id」、pagingを「true」、repeat-controlを「confirm-delete confirm-insert」、recordsを「10」、maxrecordsを「100」とします。その他のテキストフィールドは空白にします。
9Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
10Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

Post Onlyモードのページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page12.htmlを編集する」をクリックし、ページファイルのpage12.htmlを編集するページファイルエディターが開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
「http://localhost:9080」で開いたページに戻り「page12.htmlを表示するする」をクリックして、どのような画面になっているか確認してみてください。最初のテーブルは、Post Onlyモードで動作し、ボタンをクリックすると、surveyコンテキストに対して新たなレコードを作成します。2つ目のコンテキストは、単にsurveyコンテキストの全レコードを表示しているもので、入力したレコードの確認用です。

送信者、送信先、本文がすべて一定のメールの設定

 以下の演習では、メールアドレスを使用しますが、mailhogのコンテナを使っている限りは、メールアドレスに何を指定しても、実際にはメールは送られませんし、メールを送ったということをブラウザで確認できます。筆者の新居雅行のメールアドレスを記述しますが、そのまま指定しても構いませんし、ご自分のメールアドレスを指定されても構いません。

1「def12.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def12.phpを編集する」をクリックして開きます。Definition File Editorのページが開きます。右上に表示されている「Show All」のボタンを押します。Context内に表示されている項目が全て表示されます。
2surveyコンテキストの設定にあるSetting for Post-only Modeのpost-reconstructを「true」、post-dismiss-messageに「ありがとう」と入力します。post-dismiss-messageの文言はなんでもかまいません。データ送信を受けたことを示す簡潔な文言を入れてください。
3surveyコンテキストのSend Email or Messagingの設定にある、createの見出しの下に入力します。fromとtoに「msyk@msyk.net」、subjectに「ご意見承りました」、bodyに「ありがとうございます。今後ともよろしくお願いします。」を入力します。f-optionを「true」に、body-wrapは「72」とします。Tabキーを押すなどして、確実に入力をしてください。
4定義ファイルエディタでさらにスクロールして、OptionsのSMTP Settingsを表示します。ここでは送信用のメールサーバへの接続情報を指定します。serverに「mailhog」、portに「1025」と指定します。Tabキーを押すなどして、確実に入力をしてください。

フォームの入力をきっかけとしてメール送信される

1「http://localhost:9080」で開いたページに戻り、「page12.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。2つのテーブルが見えています。上のPost Onlyモードのテーブルに、適当に入力します。ここで、メールアドレスの右のテキストフィールドには定義ファイルエディタで入力したものと異なるものを指定してください。ここで、「msyk@mac.com」と入力しました。
2「送信」ボタンをクリックします。すると、まず、「送信」ボタンが消えて、コンテキストのpost-dismiss-messageキーに指定したメッセージがページ上に見えています。
3さらに4秒後にページが更新され、下側のテーブルに、今入力したデータが見えています。
4ブラウザでタブを新たに開いて、「http://localhost:8025」へ接続してください。mailhogコンテナが受け付けたメールがリストになって表示されています。
5メールの項目をクリックして、内容を表示します。このメールは、コンテキスト定義のtoに指定したメールアドレス「msyk@msyk.net」に送られており、Post Onlyモードのテーブル内で入力したメールアドレス「msyk@mac.com」には送られていません。現在の設定では、メールの送り先は常にtoに指定したメールアドレスになります。また、本文や件名、送信者は常に固定された文字列です。

データベースから得られた宛先を送信者にする

1「def12.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def12.phpを編集する」をクリックして開きます。
2すべての項目が表示されている状態でない場合には、定義ファイルエディターのページの最初にある「Show All」ボタンをクリックして、すべての項目を表示します。
3surveyコンテキストのSend Email or Messagingの設定にある、createの見出しの下に入力します。toを「@@Q2@@」と入力します。この@@でフィールドを囲む表記はテンプレート処理です。@@Q2@@の部分が、Q2フィールドに入力されている値に置き換わることを意味します。Tabキーを押すなどして、確実に入力をしてください。
4「http://localhost:9080」で開いたページに戻り、「page12.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。2つのテーブルが見えています。上のPost Onlyモードのテーブルに、適当に入力します。ここで、メールアドレスの右のテキストフィールドには「msyk@mac.com」を入力しました。
5「送信」ボタンをクリックします。5秒後にページが更新され、下側のテーブルに、今入力したデータが追加されています。
6メールが到着しています。このメールは、Post Onlyモードのテーブル内で入力した「msyk@mac.com」宛に送られています。ここで、コンテキストに設定したtoが「@@Q2@@」、つまりQ2フィールドに入力した文字列であることを思い出してください。本文や件名、送信者はここでも常に固定された文字列ですが、いずれの設定にも@@を用いてフィールドに置き換える処理が記述可能です。

メール本文にテンプレートを使用する

1IMApp_Trialによるコンテナのデータベースには、メールのテンプレートとなるテーブルと、サンプルのテンプレートメールがすでに入力されています。ターミナルから以下のコマンドを入力して、まず、MySQLへコマンドラインで接続します。
mysql -u web -h 127.0.0.1 -P 13306 --password=password test_db
2mysql > のプロンプトが出てくることを確認して、以下のSQLコマンドを入力し、テンプレートのテーブルとレコードのデータを確認します。
select * from mailtemplate;
結果は以下の図のようになっています。bodyには改行が入っていて見づらいですが、idフィールドに「1」、to_fieldフィールドに「@@Q2@@」、from_fieldフィールドに「msyk@msyk.net」、subjectフィールドに「ご意見承りました」と入力されています。bodyフィールドを追ってみると、文字列の途中に@@Q1@@など、フィールド内容に置き換わるテンプレートの記述がなされています。
3「def12.phpを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def12.phpを編集する」をクリックして開きます。
4Contextsのすぐ下にある「追加」ボタンをクリックして、コンテキスト定義をひとつ増やします。
5追加されたコンテキストでは、nameに「mailtemplate」、table、viewに「mailtemplate」、keyを「id」、とします。その他のテキストフィールドは空白にします。
6定義ファイルエディタがすべての項目が表示されている状態でない場合には、定義ファイルエディターのページの最初にある「Show All」ボタンをクリックして、すべての項目を表示します。
7surveyコンテキストのSend Email or Messagingの設定にある、createの見出しの下に入力します。from、to、subject、bodyを空欄にします。そして、template-contextに「mailtemplate@id=1」と指定します。これは、メールテンプレートとして「mailtemplateコンテキストにある、idフィールドが1のレコードを利用する」ということを意味しています。Tabキーを押すなどして、確実に入力をしてください。
8「http://localhost:9080」で開いたページに戻り、「page12.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。2つのテーブルが見えています。上のPost Onlyモードのテーブルに、適当に入力します。ここで、メールアドレスの右のテキストフィールドには「msyk@mac.com」を指定しました。
9「送信」ボタンをクリックします。5秒後にページが更新され、下側のテーブルに、今入力したデータが追加されています。
10ブラウザで「http://localhost:8025」に接続して、メールが到着していることを確認します。このメールは、Post Onlyモードのテーブル内で入力した「msyk@mac.com」に送られています。ここで、コンテキストに設定したtoが「@@Q2@@」であり、つまりQ2フィールドに入力した文字列であることを思い出してください。現在の設定では、メールの送り先は、Post Onlyモードのテーブルで入力したQ2の文字列になります。
このメールの本文は、mailtemplateテーブルのid=1のbodyフィールドの内容になっていますが、「@@フィールド名@@」の部分がフィールドの値に置き換わっています。つまり、新規作成したレコードのQ1フィールドが@@Q1@@、Q2フィールドが@@Q2@@、Q3フィールドが@@Q3@@の部分と置き換わって、メールの本文が作られています。このように、メールの本文は別に作ってある文面に、対象となるレコードの内容を差し込んで作成することができます。

演習のまとめ

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メール送信時に認証で使用するパスワード
表5-2-3 smtpキーの連想配列に設定可能なキー

 定義ファイルエディターで実際に設定する場合は、ページの冒頭にある「Show All」ボタンをクリックして、全項目を表示します。すると、図5-2-1のように、Optionsの最後に設定項目が表示されるようになります。定義ファイルへの設定を直接行う場合は、リスト5-2-1にあるように、オプション領域に記述を行います。

図5-2-1 stmpキーの値を設定した定義ファイル(定義ファイルエディター)
リスト5-2-1 stmpキーの値を設定した定義ファイル(PHPでの記述)
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の全てのプロトコルに対応してはいないので、これら以外のプロトコルの場合は必要なライブラリ等がインストールされているかなどを確認してください。必要なら、追加でインストールをしてください。

リスト5-2-2 Google Gmailを利用する場合のparams.phpでの設定
$sendMailSMTP = array(
    "protocol" => "gmail+smtp",
    "username" => "msyk.nii83@gmail.com",
    "password" => "himitsunoapripassword",
);
リスト5-2-3 Amazon SESを利用する場合のparams.phpでの設定
$sendMailSMTP = array(
    "protocol" => "ses+https",
    "username" => "yourACCESSKEY",
    "password" => "yourSECRETKEY",
);

メールのテンプレートを保存しておくテーブル

 メールをテンプレートから生成したい場合、テンプレートを表5-2-4のようなフィールド構成のテーブルに保存しておき、send-mailキー(messagingキー)以下のtemplate-contextキーに、そのテーブルからコンテキスト名を記述します。このコンテキストが定義されていて、読み出しが作成が可能な状況になっている必要があります。フィールド名はカスタマイズ等できないので、この名称を利用する必要があります。スキーマのサンプルでは、mailtemplateテーブルとして定義されています。フィールドに入れるべきデータについてはフィールド名を見れば明白です。

フィールド名
idINT(主キー)
to_fieldTEXT
bcc_fieldTEXT
cc_fieldTEXT
from_fieldTEXT
subjectTEXT
bodyTEXT
表5-2-4 メールテンプレートとして利用可能なテーブルのフィールド構成

 template-contextキーには、このテーブルのコンテキスト名に加えて、実際にテンプレートとして使用するレコードを指定します。ここで、コンテキストとして「mailtemplate」が定義されている場合、template-contextキーには「mailtemplate@id=1」のような記述を行います。@は決められた記号です。その後に、主キーフィールド名、イコールに続いて、該当するレコードのidフィールドの値を記述します。

メール送信結果を残す

 メールの記録を残したい場合は、send-mailキー(messagingキー)以下のstoreキーに、そのコンテキスト名を記述します。このコンテキストは、表5-2-5のようなフィールドを持つテーブルであって、新規レコード作成が可能な状況になっている必要があります。フィールド名はカスタマイズ等できないので、この名称を利用する必要があります。スキーマのサンプルでは、maillogテーブルとして定義されています。フィールドにそれぞれ何が入るからはフィールド名から明白です。

フィールド名
idINT(主キー)
to_fieldTEXT
bcc_fieldTEXT
cc_fieldTEXT
from_fieldTEXT
subjectTEXT
bodyTEXT
errorsTEXT
foreign_idINT(外部キー)
表5-2-5 メール送信記録に利用可能なテーブルのフィールド構成

 実際のテーブル定義では、レコード作成時の日付時刻が入る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やあるいは認証のトラブルはさらに複雑な設定が絡みます。うまく行かない場合には、以下のような原因が考えられます。

Slackのタイムラインに投稿する

 メール送信の代わりに、Slackへの投稿も可能です。まず、Slackへの投稿を行うには、「chat.postMessage」というAPIへの送信が可能なトークンを発行します。そのトークンと、投稿チャンネルに関して、params.phpファイルに以下のような設定を行います。なお、IM_Entry関数の第2引数(オプション変数)に対して、slackキーに対する連想配列で、tokenとchannelキーを与えて指定しても構いません。オプション変数の方がparams.phpよりも優先されます。

リスト5-2-4 SlackのAPI設定に関する設定
$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フィールドでリレーションシップが設定されています。

テーブル名フィールド名
registeredcontextidINT AUTO_INCREMENT,
clientidTEXT
entityTEXT
conditionsTEXT
registereddtDATETIME
registeredpkscontext_idINT
pkINT
表5-3-1 クライアント同期の動作に必要なテーブル

演習クライアント間連携の動作を確認する

クライアント間連携の機能を有効にする

 演習環境では、クライアント連携の機能は有効になっていません。まず、サーバー側での設定の変更を行います。

1Docker DesktopのContainersの画面で、php-apache_imというコンテナイメージを特定します。その行の右の方にあるOPEN IN TERMINALボタンをクリックして、ターミナルでコンテナの操作をできるようにします。ターミナルの新たなウインドウが表示されることを確認します。
2このコンテナはエディタが入っていないので、エディタをインストールします。ここでは、nanoを以下のコマンドを入力してインストールすることにします。
apt install nano -y
3インストール後、設定ファイルを修正するために、nanoで開きます。以下のようにコマンドを入力します。
cd /var/www/html/lib
nano params.php
ここで、params.phpファイルは最初から存在しているファイルで、ファイルの内容がnanoエディタで開かれていることを確認してください。
4control+Vで、先に進みます。PHPの変数$notUseServiceServerに代入している箇所を特定してください。(前方向にスクロールするのはcontrol+Tです。)
5変数$notUseServiceServerの値をfalseに、$activateClientServiceの値をtrueに変更します。
:
/* 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.
:
6保存して終了するために、control+Xを押します。保存するかどうかをたずねられるので「Y」で答えます。その後、保存するファイル名をたずねられ、読み出したファイルのparams.phpが見えているので、それを確認してEnterキーを押します。これで、変更結果が元のファイルに書き込まれました。
7ブラウザーで、「http://localhost:9080」に接続します。すでに接続していれば、そのページを表示して、command+Rやcontrol+Rなどの操作で更新を行います。
8少し時間がかかりますが、ページが出てきます。ページを最後までスクロールすると、INTER-Mediatorのバージョン表示のところに、Service Server:という項目が登場します。ここで、◆の色が赤い場合はService Serverが稼働していません。
9もう一度command+Rやcontrol+Rなどの操作でページ更新を行います。すると、◆の色が緑色になりService Serverが稼働していることが確認できます。
最初から◆の色が緑色であれば、最初からService Serverが起動しているので、それはそれで問題はありません。最初にService Serverが起動していないという表示が出ても、ほとんどの場合はService Serverは起動しています。サーバの起動が自動的になっていて、起動していない場合に自動起動する処理が状況によってはうまく稼働しないため、起動しているのに起動していないという判断をする場合もあります。なお、Service Serverはページを表示する毎に起動するのではなく、1度起動すればそれをずっと使い続けるので、最初にService Serverが起動しない点は通常の運用では問題にならないはずです。

Webアプリケーションでクライアント間同期を有効にする

 以下、Chapter 4で作成した「page01.html/def09.php」のWebアプリケーションに対して、クライアント間同期をできるように設定して動作を確認します。もし、このアプリケーションを作っていない場合には、「http://localhost:9080」で接続したページにある「サンプルプログラム」のリンクをクリックして、「Any Kinds of Samples」にある「Master-Detail Style Page」の「MySQL/MariaDB」の列にある「show」ボタンを押したページで確認をしてください。このサンプルは、ページ全体のコンテキストと、最初の繰り返し部分のコンテキストで同様な設定を行なっています。

1「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。そこにある「def09.phpを編集する」をクリックします。すでに2つのコンテキストproductとitemが存在します。いずれのコンテキストでもsync-controlの部分に「create update delete」とキータイプをしてください。設定後、Tabキーを押すなどして、入力結果を確定させてください。
sync-controlでは、ここで指定した3つのキーワードを指定でき、複数指定する場合は空白で分離します。指定した名称の操作について同期が行われます。このキーがない場合は同期処理は行われません。
2「http://localhost:9080」で開いたページに戻り、「page09.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。以前に作成した通りに表示されますが、ページ末尾のINTER-Mediatorのバージョン表示のところを見てください。緑色の◆はService Serverが稼働していることを示します。白色の➤は、ページに表示されているアプリケーションで、sync-controlの設定がなされていてクライアント間同期ができていることを示しています。

編集結果が別のクライアントに伝達されることを確認する

 クライアントの動作を確認するために、2つの異なるブラウザーを同時に起動します。SafariとFirefox、あるいはChromeとEdgeなど、INTER-Mediatorの稼働可能なブラウザーを演習環境が稼働しているOSで同時に起動します。これにより、異なるユーザーによる接続と同じ状況になります。なお、ひとつのブラウザーでウインドウが違うという状況では、厳密には「別のクライアント」と同等ではないので、2つのブラウザーを稼働させてください。

1すでにSafariで「http://localhost:9080」に接続しています。続いて、ここではChromeを起動したとします。
2Safariで見えているページのURLをアドレスバーでコピーし、Chromeのアドレスバーにペーストして、それぞれのブラウザで同一のページを参照します。以下の図では、手前のウインドウがSafari、背後のウインドウがChromeです。この段階では、当然ながら同一のデータがそれぞれのウインドウで見えています。
3変更可能なフィールドのひとつを修正してみます。ここでは、unitpriceのフィールドの値を変更してみます。手前のSafariで、unitpriceの値を変更しました。ここでは、まだ数値を変更しただけなので、背後のブラウザーでは表示内容に変更はありません。
4Tabキーを押して、修正結果を確定します。すると、背後のブラウザーの同一のフィールドが、修正後の値に自動的に変更されました。つまり、あるクライアントでの変更結果は、他のクライアントにも伝達されたということになります。

レコード追加が別のクライアントに伝達されることを確認する

1手前のSafariで、明細行の下にある「追加」ボタンをクリックします。「レコードを本当に作成していいですか?」とたずねられるので、OKボタンをクリックします。
2手前のウインドウでは新たに明細行が追加されましたが、背後のウインドウでも自動的に明細行が追加されています。つまり、あるクライアントでレコードを作成すれば、その作成されたレコードは他のクライアント側でも認識されたということです。
3追加された明細行にあるフィールドに適当な数値を入力してEnterを押すなどして確定してみました。これによりデータベースに入力した数値が記録されますが、配置のChromeの同一のフィールドでも、入力した値が自動的に入力されており、Safariでの編集結果がChrome側にも自動的に伝達されたことがわかります。

レコード削除が別のクライアントに伝達されることを確認する

1ページ上部のページネーションを操作して、どちらのウインドウにも同一のページが見えるようにします。ここでは、idが2のレコードを見えるようにしました。
2手前のSafariでページネーションにある「レコード削除:product」ボタンで削除します。削除の可否が確認されるので、OKボタンで応答して実際に削除します。
3手前のウインドウではid=2のレコードが削除されたので、ひとつ後ろのid=3のレコードが見えています。背後のウインドウでは、単にid=2のレコードが消えて存在しないので、何も表示されていない状態になりました。削除の伝達ではこういう状況にもなり得ます。

演習のまとめ

同期処理への割り込み

 クライアント同期により、伝達してきたクライアント側では、以下の6つの関数をJavaScriptで定義できます。メソッド名を見てわかるように、Create/Update/Deleteのそれぞれのオペレーションの前後つまりBefore/Afterに呼び出されます。クライアントでは、Beforeが呼び出され、更新処理を行い、Afterが呼び出されるようになります。Beforeが付くメソッドでは、引き続く処理を実行するためにはtrueを返します。逆にfalseを返すと、そこでクライアント同期の処理は停止します。引数dは、同期情報に関するオブジェクト(例:{entity: "item", field: ["product_unitprice"], 'justnotify: false, pkvalue: ["3"], value: ["30"]})が設定されて呼び出されます。

リスト5-3-1 同期処理のカスタマイズが可能なメソッド
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つのコンテキストを定義ファイルに定義

1ここからの作業は、Webブラウザー上で行います。まず、演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def13.phpを編集する」をクリックし、定義ファイルエディターでdef13.phpファイルを編集します。(もし、他の用途で13番目を利用しているのなら、例えば、def21.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6name、view、tableに「testtable」、keyを「id」、pagingを「true」、repeat-controlを「confirm-insert confirm-delete」、recordsを「10」、maxrecordsを「100」とします。
Contextsのその他のテキストフィールドは空白にします。
7Optionsのセクションにある、media-root-dirに「/var/www」と入力します。入力結果を確定させるために、Tabキーを押すなどしてください。
サーバー上では、media-root-dirキーで指定したパス以下にアップロードしたファイルが保存されます。この演習では、/var/wwwを利用します。実際のアプリケーション運用時は、適切なディレクトリを指定しますが、一般にはWebサーバーで公開していない範囲にディレクトリを作ります。また、Webサーバーのユーザーが読み書き権限があるように、ディレクトリのアクセス権を設定する必要があります。
8Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
9Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

ページファイルの作成と表示

1「http://localhost:9080」で開いたページに戻り「page13.htmlを編集する」をクリックし、ページファイルのpage13.htmlを編集するページファイルエディターが開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
ヘッダー部のLINKおよびSCRIPTタグのパスが長く見づらいですが、正しく入力してください。これらは、jQueryやBootstrap、jQueryUI、およびそれらのテーマを読み込む部分です。
INTER-Mediatorで用意しているJQueryUIのDatePicker向けアダプターは、node_modulesフォルダにあるinter-mediator-plugin-jqueryfileuploadフォルダにあります。すなわち、composerによるインストールやアップデートの後に実行されるnpmコマンドによってインストールされるものです。
タグ要素にdata-im-widget属性があり、「jquery_fileupload」という値が設定されている箇所がポイントです。ここに、JavaScriptのコンポーネントが展開されて、ファイルのアップロード機能が利用できるようになります。text1はTEXT型のフィールドです。ファイルアップロードのコンポーネントは、このように、テキスト型のフィールドとバインドした要素に記述します。この例では、TDタグに記述しています。TDタグはセルの表示はできますが、編集はできません。しかしながら、コンポーネントがその内部に必要な要素を用意するので「ファイルのドラッグ&ドロップの受付」ができるのです。しかし、単にアップロードするだけではアプリケーションとしては成り立たず、そのファイルを後から活用する必要があります。そのために、アップロードしたファイルのパスを保存するテキスト型のフィールドとのバインドを必要とします。

実際にファイルをアップロードしてみる

1「page13.htmlを開く」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新ボタン等でページを再読み込みします。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page13.htmlを開く」をクリックして開きます。ページが表示されます。レコードが存在すれば「ファイル選択」ボタンが見えるはずです。レコードがない場合は、ページネーションの「レコード追加:testtable」をクリックして、レコードを新たに作成しておきます。
2「ファイル」選択ボタンをクリックします。すると、ファイルを指定するダイアログボックスが表示されます。適当な画像ファイルを指定して、Uploadボタンをクリックします。ファイルサイズに制限がありますので、1MB程度のファイルを使って下さい。なお、この上限サイズは変更可能です(『9-4 INTER-Mediatorを利用する開発プロセス』)。
3ファイルを指定すると、「ファイル選択」ボタンの直下に選択したファイルのファイル名が見え、「アップロード」ボタンが表示されます。まだ、ファイルはアップロードされていません。ここで、「アップロード」ボタンをクリックすることで、実際にファイルをサーバーに送信します。
4ファイルのアップロードはすぐに終わります。「アップロード」ボタンは消えて、「ファイル選択」ボタンだけが表示されています。つまり、初期状態に戻りしました。表の2列目にはtext1フィールドを表示しました。そこには、アップロードしたファイルの相対パスが入力されていますが、このパスの入力もコンポーネントが自動的に行います。

アップロードした結果の確認

 ファイルがアップロードされた結果を、演習環境上で確認をします。

1Docker DesktopのContainersの画面で、php-apache_imというコンテナイメージを特定します。その行の右の方にあるOPEN IN TERMINALボタンをクリックして、ターミナルでコンテナの操作をできるようにします。ターミナルの新たなウインドウが表示されることを確認します。(以下の図は、このステップをいくつか進んでコマンドを入力した結果が見えています。)
2まず、次のようなコマンドを打ち込んで、media-root-dirに指定したパスをカレントディレクトリにします。
cd /var/www
3次のようなコマンドを打ち込んで、ディレクトリにあるファイルの一覧を表示します。ここでは、testtable、つまりコンテキスト名のディレクトリが存在することを確認します。
ls -l
4次のようなコマンドを打ち込んで、testtableディレクトリにあるすべてのファイルの相対パスを一覧表示します。id=2は、このコンテキストで、id=2によって特定できるレコードに関するファイルがあるディレクトリであることを示しています。続くtext1は対応するフィールドを示しています。data-im-widgetを書き込んだ要素のdata-im属性がtesttable@text1となっていることを思い出してください。
find testtable
testtableディレクトリ以下は、コンテキスト名(testtable)、keyキーで指定した主キーフィールドを含むレコードを特定する条件式(id=1)、フィールド名(text1)とディレクトリが階層的に構成され、最後に画像ファイルが見えていて、アップロードしたファイルをここに保持したことが分かります。ドラッグ&ドロップしたときに、text1フィールドに入力されたパスのテキストが、この画像ファイルのパスになっていることを確認してください。
もともと、ここにドラッグ&ドロップした画像のファイル名は「313.jpg」でしたが、アップロード時にファイル名に4桁のランダムな数字(ここでは「6317」)を付与し、既存のファイルの上書きがなされないようにしています。

アップロードしたファイルの履歴を残す

 ここまでの方法では、あるレコードのあるフィールドに対して、アップロードしたファイルのうち、最後のファイルへのパスだけがデータベースに残っていました。さらに発展させて、ファイルをアップロードした履歴も残すようにします。

1「def13.phpを開く」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新ボタン等でページを再読み込みします。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「def13.phpを開く」をクリックして開きます。
2Contextsのすぐ下にある「追加」ボタンをクリックして、コンテキスト定義をひとつ増やします。
3name、table、viewに「fileupload」、keyを「id」します。その他のテキストフィールドは空白にします。
4Relationshipのすぐ下の「追加」ボタンをクリックして、設定項目を増やし、foreign-keyを「f_id」、join-fieldを「id」、operatorを「=」、portalを空白とします。
5定義ファイルエディターの一番最初にあるShow Allボタンをクリックして、すべての設定項目を表示します。
6既存のコンテキスト(testtable)で、File Uploadingと記述された部分の下にある「追加」ボタンをクリックします。
7新たに追加された項目で、fieldを「text1」、contextを「fileupload」、containerを空欄とします。
8「page13.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page13.htmlを編集する」をクリックして開きます。
9次のように、ページファイルのコードを修正します。テーブルに、新たにfileuploadコンテキストを展開するテーブルを定義します。
<!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>
10「page13.htmlを開く」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新ボタン等でページを再読み込みします。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page13.htmlを開く」をクリックして開きます。
11ページネーションのコントローラーにある「レコード追加」のボタンをクリックして、新たにレコードを追加します。
12新しく作られたレコードの「ファイル選択」ボタンをクリックして、ファイルを指定します。ファイルを指定すると、「アップロード」ボタンが見えるようになります。
13「アップロード」ボタンを押してアップロードが終了すると、パスが2つ追加されました。左側がTD要素に展開したtext1フィールドで、右側はfileuploadコンテキストのpathフィールドです。
14同じレコードの「ファイル選択」ボタンをクリックしてファイルを指定して「アップロード」ボタンを押し、さらにファイルを追加します。text1フィールドは最新のファイルのパスのみが残っていますが、fileuploadコンテキスト側では、アップロードしたファイルのパスを随時記憶しています。
15ファイルが保存されている様子を確認します。Docker DesktopのContainersの画面で、php-apache_imというコンテナイメージを特定します。その行の右の方にあるOPEN IN TERMINALボタンをクリックして、ターミナルでコンテナの操作をできるようにします。ターミナルの新たなウインドウが表示されることを確認します。
16以下のようにコマンドを打ち込んで、アップロードされたファイルなどを一覧します。
cd /var/www
find testtable
例えば、次のようにファイル一覧が表示されます。id=3の新しいレコードに対して、text1にバインドした要素にあるアップロードボタンから、3つのファイルがアップロードされて、それぞれがファイルに残っていることが分かります。text1フィールドには最後のP1100099_3235.JPGしか残っていませんが、fileuploadコンテキストのテーブルには、この3つのパスがいずれも残っています。
# 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ページの中で表示する方法を説明します。なお、この方法は、アップロードのコンポーネントを使わないでアップロードしたファイルについても適用できる手法です。

1「page13.htmlを編集する」をクリックして表示したタブあるいはウインドウに戻ります。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page13.htmlを編集する」をクリックして開きます。
2次のように、ページファイルのコードを修正します。fileuploadコンテキストを展開するテーブルに、さらに画像を表示するIMGタグを追加します。
<!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>
IMGタグのsrc属性は「定義ファイル?media=」で記述し、=以降は、ファイルの存在するパスを、media-root-dirキーの値のディレクトリからの相対パスで指定します。このパスはpathフィールドに入っています。ターゲット指定に#をつけることで、pathフィールドの値を現在のsrc属性の後につなげます。
3「page13.htmlを開く」をクリックして表示したタブあるいはウインドウに戻り、ブラウザーの更新ボタン等でページを再読み込みします。もし、閉じていたら、「http://localhost:9080」で開いたページに戻り「page13.htmlを開く」をクリックして開きます。読み込んだ画像がページ上に見えています。

演習のまとめ

ファイルアップロードのその他の機能

 ファイルのアップロード履歴を、演習では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
表5-4-1 サンプルにあるJavaScriptコンポーネント「TinyMCE」
コンポーネント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
表5-4-2 サンプルにあるJavaScriptコンポーネント「CodeMirror」
コンポーネント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
表5-4-3 サンプルにあるJavaScriptコンポーネント「jQuery DatePicker」
コンポーネント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
表5-4-4 サンプルにあるJavaScriptコンポーネント「flatpickr」
コンポーネント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
表5-4-5 サンプルにあるJavaScriptコンポーネント「jQuery File Upload」
コンポーネント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
表5-4-6 サンプルにあるJavaScriptコンポーネント「Popup Selector」
コンポーネントJSON Formatter(JSONを整えて表示する。独自開発)
レポジトリhttps://github.com/inter-mediator/inter-mediator-plugin-jsonformatter
data-im-widgetの値jsonformatter
ページファイルなし
定義ファイルなし
表5-4-7 サンプルにあるJavaScriptコンポーネント「JSON Formatter」
コンポーネント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
表5-4-8 サンプルにあるJavaScriptコンポーネント「Mermaid」
コンポーネント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
表5-4-9 サンプルにあるJavaScriptコンポーネント「QRCode」

メディアファイルの内容の取得

 定義ファイルは、通常はフレームワーク自体をページファイルに送り込むことや、データベースアクセスに利用しますが、他にもさまざまな機能があります。そのうちのひとつが、ファイルの内容を取り出す仕組みです。HTMLでは、IMGタグによる画像や、PDFファイルへのリンクといった用途に使うことを想定しており、パスを与えて、そのファイルの中身をMIMEタイプなどとともにクライアントに返すといった動作を行います。基本的にはリスト5-4-1のような記述を行います。mediaというキーでパラメーターを指定するということです。

リスト5-4-1 定義ファイルを利用してファイルの内容を取り出す
一般的な記述:定義ファイルへのパス?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-1 データベースで利用するtesttableレイアウト

 このオブジェクトフィールドには計算値自動入力の設定がなされています。フィールド定義を調べるには、「ファイル」メニューの「管理」から「データベース」を選択して、データベースの管理ダイアログボックスを表示します(図5-4-2)。この式については、FX.phpを利用している場合に、アップロードとダウンロードをうまく整合させるために設定した式です。FileMaker Data APIを利用する場合にはこの式の設定は必要ありません。

図5-4-2 使用するテーブルtesttableのフィールド定義

 ここから、定義ファイルとページファイルを用意します。それぞれ、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」としておきます。

図5-4-3 定義ファイルの変更箇所1

 続いて、定義ファイルエディタの最初のところにあるShow Allボタンをクリックして、項目をすべて出しておきます。そして、図5-4-4のように、testtableコンテキストのFile Uploadingのところで「追加」ボタンをクリックして新たな行を追加し、fieldに「vc1」、containerに「true」を入力しておきます。

図5-4-4 定義ファイルの変更箇所2

 さらに、Optionsの設定では、図5-4-5のように、media-root-dirに「/var/www」と設定をしておきます。

図5-4-5 定義ファイルの変更箇所3

 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-6 定義ファイルの変更箇所4

 ページファイルをリスト5-4-2のようにします。ファイルアップロードのコンポーネントをdata-im-widget属性で指定するタグ要素は、vc1フィールドにバインドします。そして、vc1フィールドに入力されている画像を表示するには、vc1フィールドの値をmedia=の後につなげます。オブジェクトフィールドは、カスタムWeb経由でデータを得ると画像等のバイナリデータではなく、フィールドに入力することが可能なURLの一部分が得られます。それを定義ファイルの引数に与え、定義ファイルから先のINTER-Mediatorの内部で正しいURLを構築して画像データなどを得ています。

リスト5-4-2 修正したpage14.html
<!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です。

図5-4-7 ページファイルを表示した

>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」に置き換える状態にしておきます。

リスト5-4-3 Amazon S3を利用する場合にparams.phpファイルに定義する変数
$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というテキストファイルで、内容は以下のような形式です。この形式を連続して記録することもできます。[ ] の部分がプロファイル名になります。

リスト5-4-4 ~/.aws/credentialsファイルの内容例
[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の中で画像などのファイルを扱うだけであれば、このように「パブリックアクセスをすべてブロック」している状態で問題はありません。

図5-4-8 ブロックパブリックアクセス (バケット設定)の設定

 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」という値が指定されているようにする必要があります。

リスト5-4-5 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からリフレッシュトークンの取得が可能です。途中、コンソールの指示に従っての操作が入り、ブラウザを操作してコードを得て入力するなどの作業が必要になります。

図5-4-9 EclipseでJavaプログラムを稼働させてリフレッシュトークンを取得した

 ここまでの作業で得られたAppKey、AppSecret、リフレッシュトークンの3つは、リスト5-4-6のように、params.phpファイルの変数として定義しておきます。これらから発行されるアクセストークンは、$dropboxAccessTokenPathで指定したファイルに記録します。したがって、このファイルは、Webサーバのアカウントによって書き込み可能になっている必要があります。例えば、Ubuntuでは、www-dataによって書き込み可能になっている必要があります。/var/www以下に適当なファイルを作って、アクセス権の設定をコマンド等で行なっておきます。Dropbox上で、$rootInDropboxで指定されたパス以下にファイルが書き込まれます。なお、当然のことですが、アプリを生成したときに使われたDropboxの領域内に、ファイルは保存されます。

リスト5-4-6 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」という値が指定されているようにする必要があります。

リスト5-4-7 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のコードを参考にしてください。

リスト5-4-8 CSVファイルをアップロードする機能のページファイル内の要素の例
<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でも指定が可能です。設定可能なキー等はこの後に詳細に説明します。

リスト5-4-9 定義ファイルでの指定例
// コンテキスト定義での定義例
["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
表5-4-10 インポート処理に指定できる設定

 まず、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キーと同一のテーブルがあるといった状況を想定してください。

リスト5-5-1 クロステーブルで使用する3つのコンテキスト
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属性によってターゲット指定が記述されています。ここで、Ⓑのセルは行見出し用コンテキスト、Ⓒのセルは列見出し用のコンテキストが展開されます。したがって、これらのセルでは、コンテキスト定義に存在するコンテキスト名およびその中に存在するフィールド名を記述する必要があります。そして、Ⓓのセルには、交差セル用コンテキストに対応したターゲット指定を記述しますが、セルを埋めるルールは、この後の『クロステーブル生成の仕組み』で説明をします。なお、Ⓐのセルは、ページ合成後もテーブルの左上のセルとして配置されます。

図5-5-1 ページファイルに用意するテーブルの中身
リスト5-5-2 ページファイルへの記述例
<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タグは、ページファイルにあったもではなく、プログラムで単に生成したものです。Ⓐのセルは単に複製して子要素にするだけですので、この中にターゲット指定を記述しても、データベースの内容を展開することはありません。

図5-5-2 テーブルにⒶのセルを合成する

 続いて、テーブルの1行目に列見出しを作成します。図5-5-3のように、1行目のTRタグ要素をエンクロージャー、Ⓑのセルをリピーターとして、ページ合成を行います。ここではⒷのセルの中身を解析して、ターゲット指定の設定を集めて、一番多く使われているコンテキスト名を決定し、そのコンテキスト名の定義に基づいてデータベース処理を行います。つまり、通常のエンクロージャー/リピーターの展開が行われます。そして、コンテキストオブジェクト自体も生成されます。もちろん、クエリー結果のレコードの個数だけⒷのセルが複製されて、その中ではフィールドの内容がタグ要素に合成され、データベースのデータがセルに見えることになります。もちろん、複数のリンクノードを記述して複数のフィールドを指定しても構いません。なお、内部にさらにエンクロージャー/リピーターが見つかれば展開は行いますが、一般には処理速度を考慮すべきところであり、1回のデータベースアクセスで必要なデータを取り出すようにデータベース側を設計しておくことが求められます。

図5-5-3 テーブルに列見出しを合成する

 次にテーブル2行目から、行見出しの合成を行います。図5-5-4のように、TBODYタグをエンクロージャーとし、ⒸのセルをTRタグで包んだ要素をリピーターとして通常通りの手順で合成を行います。したがって、Ⓒのセルに含まれるリンクノードのターゲット指定によってコンテキスト名が決まりデータベースアクセスして、取り出されたレコードのフィールドの値が2行目以降の1セル目に合成されます。Ⓒのセルを包むTRタグ要素も、元からページファイルにあったものではなく、単にプログラムで生成したものです。Ⓒのセル内に複数のリンクノードを設けても構いませんし、さらにその中にエンクロージャー/リピーターのセットがあれば展開を進めるのも同様です。

図5-5-4 テーブルに行見出しを合成する

 そして、2行目の2列目以降、列見出しのレコード数分Ⓓのセルを付加します。最初は単にセルを付加して、行列をセルで埋める作業を行います。そして、Ⓓのセルを解析してコンテキスト名を求めてコンテキストを決定し、データベースアクセスを行います。クエリー結果のレコードを順番に合成するのではなく、順番に調べて置き場所がテーブル内にある場合、その交差セルに対して合成、つまりリンクノードの指定に応じてフィールドの値をタグ要素内に埋め込む処理を行います。したがって、交差セル用コンテキストで得られた結果でも、対応する行あるいは列がなければ無視されます。この処理はクロステーブルだけで行われており、エンクロージャー/リピーターによる展開とは異なる処理が組み込まれています。したがって、エンクロージャーに相当するノードはないため、図の中ではエンクロージャーに対しては「規定なし」と記載をしました。なお、Ver.5.4-dev現在の実装では、交差セルへの合成は、単に合成するだけなので、同一のセルに2回以上合成すると、それらの数値の文字列がつながって見えるだけです。加算等は実装されていませんので、コンテキストから得られるデータは集計結果になっているものを利用するようにしてください。

図5-5-5 テーブルを交差セルで埋める

 最後の交差セルを埋める部分はデータを見ながら説明をしましょう。サンプルファイルはSamples/Practice/crosstable.htmlおよびcrosstable.phpにあります。演習環境を利用している場合には、ブラウザーで「http://localhost:9080」に接続し、そこにある「サンプルプログラム」のリンクをクリックして、サンプルプログラムの一覧を表示します。そこにあるPracticesにある「cross table」の項目をクリックして、動作を確かめることができます。図5-5-6は、そのサンプルを動かした結果です。このサンプルのページファイルは、このセクションで示したページファイルの記述のサンプルと同一のものです。定義ファイルには、item、customer、salesummaryの3つのコンテキストが指定されていて、それぞれ、列見出し、行見出し、交差セルのコンテキストです。

図5-5-6 クロステーブルのサンプルプログラム

 以下、3つのコンテキストの具体的な値をもとに、クロステーブルの動作を検討しましょう。列見出しのコンテキスト「item」と、行見出しのコンテキスト「customer」に関して、クエリー結果のデータを示すと、表5-5-1と表5-5-2のような結果になります。それぞれの値が、セルの中に表示されているのが分かります。

idname
25Onion
26Parsnip
27Peppers
28Potato
29Pumpkin
30Peas
31Rhubarb
32Shallots
33Spinach
34Squash
35Sweet Potato
表5-5-1 コンテキスト「item」へのクエリー結果
idname
250Danio Food, Co.
251Darter Food, Co.
252Dartfish Food, Co.
253Dealfish Food, Co.
254Death Valley pupfish Food, Co.
255Deep sea anglerfish Food, Co.
256Deep sea bonefish Food, Co.
257Deep sea eel Food, Co.
258Deep sea smelt Food, Co.
259Deepwater cardinalfish Food, Co.
表5-5-2 コンテキスト「customer」へのクエリー結果

 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キーでの指定から得るようにしました。

iddtitem_idcustomer_idqtyunitpricetotal
12010-01-01 00:00:003854975283696
22010-01-01 00:04:45246321678112496
32010-01-01 00:14:103025183732984
42010-01-01 00:27:02324964633115226
:::::::
942010-01-01 22:02:291686044721888
952010-01-01 22:24:34272554129712177
:::::::
表5-5-3 コンテキスト「salessummary」へのクエリー結果

 クロステーブルで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」を適用した状態と同じにする
表5-6-1 INTER-Mediatorに付属のテーマ

 テーマの設定は、以下のいずれかの方法で行えます。IM_Entry関数はもちろん定義ファイルに記述するもので、このテーマの設定は、定義ファイルを参照しているページにだけ適用されます。params.phpファイルに指定すると、そのファイルを参照しているINTER-Mediator全体に適用されるので、例えばサイト全体をまとめて設定したいときに利用できます。

 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-2 ページネーションで割り当てられたスタイルシートのセレクタ

 ここでボタンの背景と、黄色いマーキングを別のものに変えたいとします。色のセンスはさておいて、ボタンを白背景の赤文字にしたいとした場合、リスト5-6-1のようにヘッダー部にstyleタグ要素を追加して、ボタンに対するCSSの設定を上書きしてしまいます。

リスト5-6-1 ページネーションの要素に対する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変数も記述します。

リスト5-6-2 独自のテーマを独自のディレクトリに配置するときのparams.phpの一部
$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ファイルの中身をマージして返します。

リスト5-6-3 テーマの中身を取り出すURL
[定義ファイル名]?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-1 ステップ動作を示すサンプルアプリケーション

 「東京都」をタップすると、東京都の市区町村名が一覧されます(図5-7-2)。ここで、まず、市区町村のリストもテーブルで構築していますが、スクロールすることを確認してください。その時、タイトルバーとページ下部の表示は画面に固定され、その間でスクロールされるという典型的なモバイルアプリケーションの動作になっていることを確認してください。この動作はINTER-Mediatorとは関係なく、CSSの設定で可能です。これについても、あとで説明します。また、ヘッダーの左側に、◀︎ボタンが表示され、これをタップすると、東京都などの最初のリストが表示されることが分かります。つまり、これは「戻る」ボタンであり、最初の画面では表示されていないことも確認してください。

図5-7-2 次の画面に移動すると「戻る」ボタンが表示される

 「市区町村」をタップしたあとは、町域名の一覧が表示されます。この時、前に選択した市区町村に含まれる町域名だけが表示されていることを確認してください。つまり、前のステップの選択肢が、次のステップの一覧表示に対して影響を与えている、つまり検索条件を与えるということができています。そして、最後は、郵便番号、都道府県、市区町村、町域名が一覧され、そこから先にはステップ動作での移動は行いません(図5-7-3)。戻ってまた別の場所を選択することもできます。郵便番号が見える画面では、いずれのセルをタップしても画面遷移は行いませんが、デバッガのコンソールにオブジェクトの内容が出力され、これまでの4つのステップで、何をタップしたのかが記録されていることが分かります。ここで、記録されている状況を例えばデータベースに書き込むなどすることになることも多いかもしれませんが、その作業はJavaScriptでプログラムを記述する必要があります。

図5-7-3 最後の画面のセルをタップすると、コンソールで遷移の履歴が参照できる

ステップ動作のためのコンテキスト定義

 定義ファイルの内容を参照しましょう。定義ファイルは、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コンテキストが選択され、このコンテキストを利用しているページファイルの一部分が更新され、データベース処理が行われてクエリー結果をリピーターに合成します。そして、「次へ」という動作の時にスタック動作を行うグローバル変数に情報を残すので、「戻る」ことも自動的に行えるようになっています。

リスト5-7-1 ステップ動作のサンプルのコンテキスト定義
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属性に必ず前述の名前を指定します。それが、ページ内の見えている場所に配置されていれば構いません。通常はひとつの要素だけで十分と思われますが、複数あってもかまいません。

リスト5-7-2 ページファイル内でのページ合成を行う部分
<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変数のオブジェクトとして定義します。引数はありません。

リスト5-7-3 doAfterPrefSelectionメソッドの定義
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-1 before-move-nextstepキーで指定するメソッドの返り値

 コンテキスト定義内には、表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次のコンテキストに移行する直前
表5-7-2 コンテキスト定義で指定できるステップ動作関連のメソッド

 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メソッドと同じかどうかを判定して、ボタンから次のステップに移動するのか、セルをタップするのかを判別することができます。

リスト5-7-4 moveNextStepメソッドでのステップ移動をbefore-move-nextstepキーで指定したメソッド内で判定する
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ステートメント
prefectureSELECT MIN(id) AS pref_id, f7 AS pref FROM postalcode GROUP BY f7
citySELECT MIN(id) AS city_id, f8 AS city FROM postalcode GROUP BY f8
townSELECT MIN(id) AS town_id, f9 AS town FROM postalcode GROUP BY f9
wrapupSELECT * FROM postalcode
表5-7-3 それぞれのコンテキストで実行される基本のSQLステートメント

 まず、最初の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_idpref注釈
1東京都SQL文で得られた結果
101埼玉県appending-dataキーで追加された結果
102神奈川県appending-dataキーで追加された結果
103千葉県appending-dataキーで追加された結果
表5-7-4 prefectureコンテキストで得られるリレーション

 ここで「東京都」をタップしたとします。すると、リスト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_idcity
1千代田区
447中央区
605港区
表5-7-5 cityコンテキストで検索条件「f7 = '東京都'」を付与して得られるリレーション

 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にしています。

リスト5-7-5 ページファイル内に記述されたスタイルシートの一部
#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の高さを求めて設定をしています。なお、このプログラムは、ページデザインを変更した場合など、ヘッダーやフッターの状況によって作り変えが必要になります。

リスト5-7-6 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にして、座標位置や幅を数値で与えています。

リスト5-7-7 そのほかのスタイル設定
.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ではできません。

定義ファイルエディターを開き最初のコンテキストを入力

1ここからの作業は、Webブラウザー上で行います。まず、演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def15.phpを編集する」をクリックし、定義ファイルエディターでdef15.phpファイルを編集します。(もし、他の用途で15番目を利用しているのなら、例えば、def21.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3ページ上の「Show All」ボタンをクリックして、全ての項目を表示します。
4Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
5「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
6同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
7nameを「citylist」、tableを「dummy」、viewを「postalcode」、keyを「city_id」、pagingを空欄、repeat-controlを空欄、navi-controlを「step」、recordsを「10000」、maxrecordsを「10000」、before-move-nextstepを「doAfterCitySelection」とします。
さらに、Aggregation Query Accessにあるselectを「MIN(id) AS city_id, f8 AS city」、fromを「postalcode」、group-byを「f8」とします。

2つ目のコンテキストを入力

1引き続き定義ファイルエディターでの作業を続けます。Contextsのすぐ下にある「追加」ボタンをクリックして、新たにコンテキスト定義を追加します。
2nameを「opinion」、tableを「dummy」、viewを「postalcode」、navi-controlを「step-hide」、recordsを「1」、maxrecordsを「1」、before-move-nextstepを「doAfterOpinion」とします。その他は空欄にします。
3Database Settingsにあるdb-classは「PDO」のままでかまいません。dsnには「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
4Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行っても構いませんが、ブラウザーをモバイルシミュレーション動作させるとページ上でのデバッグ情報の参照ややりにくくなるので、コンソール等で参照してください。

ページファイルの修正

1「http://localhost:9080」で開いたページに戻り「page15.htmlを編集する」をクリックし、ページファイルのpage15.htmlを編集するページファイルエディターが開きます。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
2最初にヘッダー部に、metaタグの要素をひとつ追加します。
<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>
3ヘッダー部の、def15.phpを含むscriptタグの次に、以下のプログラムをscriptタグで囲んで記載します。なお、最初の方は、サンプルプログラムのページファイルにあるものと同一ですので、そちらからコピーしてペーストしましょう。後半は入力します。太字の部分は入力、それ以外の部分はサンプルファイルからのコピー&ペーストを行います。
citylist、opinionのそれぞれのコンテキストで定義されたbefore-move-nextstepキーの値がメソッド名になります。INTERMediatorOnPage変数のオブジェクトに、そのメソッドを定義します。citylistでのタップにより、その時にcity_idフィールド値を次のopinionコンテキストの検索条件に設定しています。opinionコンテキストは最後のページなので、遷移しないようにfalseを返しています。
<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>
4ヘッダー部の、末尾に以下のスタイルシートをstyleタグで囲んで記載します。なお、最初の方は、サンプルプログラムのページファイルにあるものと同一ですので、そちらからコピーしてペーストしましょう。後半は入力します。太字の部分は入力、それ以外の部分はサンプルファイルからのコピー&ペーストを行います。
ボタンとテキストフィールドの枠線が消えてしまうので、そのためのスタイルを追加しました。
<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>
5ボディ部は以下のように記述します。bodyタグ以外は全て手入力する必要があります。
<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>
テキストエリアにあるdata-im属性は、ローカルコンテキストを利用することを示しています。ターゲット指定の最初の部分であるコンテキストの指定が「_」であれば、ローカルコンテキストです。

Chromeを利用してモバイルシミュレーションで稼働させる

1ここからの作業は、Chromeを使います。ここまで別のブラウザーで作業をしていて、ここからChromeを使うこともできます。「http://localhost:9080」で開いたページに戻り、「page15.htmlを表示する」をクリックして、ページを開きます。
2デベロッパーツールを開きます。Macだと、command+option+I(アルファベットの「アイ」)、Windowsのだとctrl+shift+Iです。
3デベロッパーツールのElementsの左にあるモバイルツールボタンをクリックするなどして、画面をモバイルシミュレーションにします。必要に応じて、ツールの設定後に更新を行います。最初のページが表示され、市区町村の一覧が見えています。
4適当なセルをタップして、次の画面に移動します。テキストエリアが見ているので、適当に入力します。
5「アンケート結果を送る」ボタンをクリックすると、ダイアログボックスに、最初の画面での選択結果と、次の画面での入力値が表示されました。
ダイアログボックスで表示する部分は、finishSurvey関数です。ここで例えば、データベースに書き込むスクリプトを記述すれば、アンケート結果の保存ができるようになります。データベース処理の記述は、『6-3 データベースへの書き込みを直接行う』で説明しています。

演習のまとめ

このセクションのまとめ

 ステップ動作は、ひとつのコンテキストごとに画面に表示する機能で、ある種のモバイルアプリケーションでよく見られる形式であるとも言えます。画面の構築はもちろん、タップ後の画面遷移もコンテキスト定義に定義されている順序で順番に行われます。また、戻るボタンの動作も自動的に行われます。タップ時には定義したメソッドを呼び出せるので、そこでさまざまな処理を記述できますし、遷移をやめたり順序と関係ない別のコンテキストに遷移したり、さまざまな動作を実装できます。ただし、JavaScriptのプログラミングが必要になります。

5-8コピー結果を残すルックアップ

FileMakerのルックアップと同等な機能をINTER-Mediatorでは実装しています。ルックアップは別のコンテキストの値をコピーする機能です。Accessにあるルックアップはポップアップメニューを構築する機能なのでここでのルックアップとは異なります。INTER-Mediatorでは「ルックアップ」と言えば、FileMakerのルックアップ機能を示すものとします。

「ルックアップ」の意味について

 まず、ルックアップの動作について概念的に説明をしますが、あまり抽象的だとイメージが湧かないので、表の上でのサンプルで説明をします。図5-8-1のようなテーブルがあったとします。「販売明細」テーブルは、伝票で言えば明細として繰り返して表示されるレコードを管理する部分です。ここで、「販売明細」テーブルとは別に、商品マスターとしての「商品」テーブルがあったとします。

図5-8-1 よくある商品マスターを使ったリレーションシップ

 図5-8-1の下半分にあるように、それぞれの「商品ID」フィールドの値を元にテーブル結合すれば「販売明細」の各行で、それまでにはなかった「商品名」と「単価」が得られて、例えば、伝票として実際に表示する場合に商品名が明細内部に見えるようになったり、単価と個数をかけて金額を求めるということができるようになります。

 データベースの正規化理論では当たり前のことです。このようなマスターを別テーブルに用意すると、例えば、単価が変われば、マスターだけを変更すると、それを参照している明細全てて単価が変更できます。ただ、これはそのようにしたい場合はそれでいいのかもしれませんが、逆に、単価が変わっても、すでに発行した伝票の明細の単価は変わってほしくない場合もあります。どちらが正しいということではなく、これはそのアプリケーションに必要な要求がどちらかということです。前者のような状況では、正規化したテーブルを用意すればいいのですが、後者のような場合どうすれば良いかはなかなか難しく、よくある方法は、単価が期間で決定される場合には「商品」テーブルにフィールドを増やして「単価の有効期間」的な概念を導入して、うまく処理をする必要があります。ここではその話はメインではありませんので、できるけども難しいというところで話を終わらせます。

 このような「その時点での単価を記録したい」というニーズに対応するのがルックアップです。図5-8-2を参照してください。ここでは、「販売明細」に「商品名」「単価」というフィールドがありましたが、図5-8-1では空欄のままでした。これらのフィールドに、「商品」テーブルの現在の値をコピーします。その時、「商品ID」がコピーする元データを取り出す手掛かりになります。もちろん、「販売明細」の「商品ID」を元に、同一の値を持つ「商品」テーブルのレコードを探して、該当するフィールドをコピーするということを行います。

図5-8-2 ルックアップの仕組みにより「販売明細」に商品名と単価をコピーした

 この仕組みを一般的に利用できるようにしたものが、「ルックアップ」です。ここでは説明のために、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のとおりです。

図5-8-3 明細を作った直後

 明細の中は、明細の1行を管理するitemテーブルを元にしたitemコンテキストと、商品マスターであるproductテーブルを元にした、productおよびproduct_listコンテキストがあります。productコンテキストはルックアップを実施するために追加で必要なコンテキストです。product_listはポップアップメニューの選択肢のために利用します。明細の最初のセルを理解するのに必要な部分をリスト5-8-1に示しました。

リスト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のレコードの値が入っています。

図5-8-4 ポップアップメニューで「Orange」を選択

 ここでは、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列目もフィールド違いますが、基本的には同じ動作になります。

リスト5-8-2 リストの3、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フィールドにコピーされていて、それを修正しているので、マスター側には変更の影響は及ばないということになります。

図5-8-5 product_nameの値を変更する

ルックアップを実行するための設定

 ルックアップを設定するためには、data-im-control属性の設定が必要ですが、それに至るまでに、コピー先のフィールドの用意、リレーションシップ先のコンテキスト定義など、さまざまな作業が発生します。自分自身のバインドとも関係あるため設定はややこしいですが、可能な限りシンプルに設定できいるように考えました。

 data-im-control属性は次のようなルールで記述します。まず、最初に「lookup」という決められたキーワードを記述し、続いて、半角のコロン(:)で区切って、lookup意外にさらに2つの記述を行います。lookupを記述するのは第一パートと呼ぶことにします。第二パートは、変更が発生するフィールドを、「コンテキスト名@フィールド名」つまりターゲット指定の形式で指定します。第二パートで指定したフィールドが変更されると、第三パートで指定した「コンテキスト名@フィールド名」のフィールドの値を、自分自身の要素にバインドしているフィールドにコピーします。第3パートに出てくるコンテキストは、原則として第二パートのコンテキストとの関連付けが成り立つようなrelationキーの定義が必要になります。第2、第3パートは、フィールド名以降は何も指定しないでください。ターゲット指定と書きましたが、実際にはコンテキストに含まれるデータベースから取り出した値になり、要素に関連する指定ではないからです。

このセクションのまとめ

 リレーションシップの先からデータをコピーしてフィールドに設定するルックアップの仕組みは、INTER-Mediatorではユーザインターフェース上で実現しています。ひとつのレコードに対して、該当するフィールドが更新されると、別のフィールドが別のコンテキストから取り出した値で埋められるという動作になります。ルックアップの仕組みは意外に便利に使える場面があるので、データベース設計では考慮する必要がある仕組みです。

5-9アプリケーションのローカライズ

 ブラウザの有線言語に応じて、ページ上のメッセージを切り替える仕組みをここでは「ローカライズ」と称します。見出しや表のラベルなど、常に一定で良い文字列もありますが、場合によってはデータベースの出力結果を言語ごとに文字列を切り替えるということをやりたいかもしれません。これらの仕組みを紹介します。

ローカライズが可能な機能

 ローカライズの機能は、ひとつではなく、いくつかの機能で実現しています。また、「新規レコード」ボタンの名前を置き換えるカスタマイズの機能(コンテキスト定義に記述するbutton-namesキー)も、ある意味で、状況に応じた文字列の置き換えではありますが、ここでは、ブラウザの言語に応答する機能に絞ることにします。次のような機能があります。それぞれ、順番に説明をします。

システムが生成する文字列とそのローカライズ

 「挿入」や「削除」のボタンがありますし、場合によってはアラートボックスで何かメッセージが出てきます。それら、システムが利用する文字列(以下、「システムメッセージ」と呼びます)は、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番のメッセージ(非対応のブラウザを使っている場合のメッセージ)を代入した文字列に置き換えることができます。

リスト5-9-1 params.phpでシステムメッセージを置き換える
$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キーの値を取り出します。配列をフラットに定義するだけでなく、分類ができるようになっていると考えてください。

リスト5-9-2 要素にdata-im-locale属性を指定する
<h1 data-im-locale="page|title">Contact Management</h1>
<table>
    <tr><th data-im-locale="category">category</th>....
リスト5-9-3 IM_Entry関数の第2引数にtermsキーを指定する
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以下の配列のキーにない値が得られら場合は、その値そのものが利用され、つまりはローカライズ対象外となります。

リスト5-9-4 要素にdata-im-locale属性を指定する
<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>
リスト5-9-5 IM_Entry関数の第2引数にtermsキーを指定する
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変数に定義して、システム全体に同一の変換テーブルを与えることもできます。

リスト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' => 'チェック',
    ],
];

このセクションのまとめ

 ページ内の要素の文字列を接続してきた言語ごとに異なるものにすることができます。つまり、これによって、ブラウザの言語に応じて、異なる文字列をページ上に表示することができ、いわゆるローカライズの仕組みが利用できます。要素そのものの値を言語ごとに切り替えるだけでなく、データベースから取り出した値のローカライズも可能です。