Chapter 8
サーバーサイドでのプログラミング

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

この章では、サーバーサイドで稼働するスクリプトのプログラミングを解説します。アプリケーションの実行のために、サーバー上で動くPHPのプログラムを記述できます。なお、PHPのプログラミングについての詳細は、PHPのマニュアルサイトや解説書等をご覧ください。

8-1サーバーサイドでの処理の追加

このセクションでは、サーバーサイドでの拡張機能の概要を説明します。本来、INTER-Mediatorでは色々な意味での拡張を持つようにしてきましたが、たくさんの案件をこなす中、PHPのプログラムを利用しての拡張は以下の3パターンに集約されます。汎用的な意味での拡張は、このチュートリアルのVer.5対応版『8-3 サーバーサイドでの処理の追加』で説明をしているので、そちらをご覧ください。このバージョンのチュートリアルは、アプリケーション利用の観点からの説明とします。

 最初の「アドバイス定義クラス」は、複雑なロジックを実装することを意図したものです。あるデータベース処理の前後に、定義したプログラムを実行できます。例えば、あるテーブルのレコードを作るときに、その関連レコードについても同時に新たに作るような場合に、数行のプログラムで対処が可能です。この場合は前者のテーブル、すなわちコンテキストに対するCreateのデータベース処理に対して、レコードが問題なく作成されれば、アドバイス定義クラスとして、関連レコードの新規作成を行えば良いのです。定義したメソッドは、前者のテーブルのレコードの値が得られるので、リレーションシップを確保することもプログラム上で記述可能です。

 2つ目の「メディア拡張クラス」は、「定義ファイルへのURL?media=パス等」による機能を拡張できる方法で、media=以降をclass://とすることで、特定のプログラムを実行できます。例えば、伝票のPDFを生成するようなプログラムを持つクラスを指定すれば、データベースの値を元にPDFを生成するようなことができます。また、CSV等でエクスポートするような仕組みも、このメディア拡張クラスで実現できますが、CSVエクスポートはINTER-Mediatorに汎用的なメディア拡張クラスを定義してあり、プログラムを全く記述しなくてもCSV出力は可能ですが、カスタマイズが必要であれば、汎用クラスを継承するなどしてプログラムコードの記述が必要になるかもしれません。

 ここまでの2つは、INTER-Mediatorの枠内での「拡張」を行う仕組みですが、3つ目の「独立したスクリプト」は文字通り、独立したものであり、PHPである必要はありません。しかしながら、INTER-Mediatorで作っているアプリケーションの内部を調べたり、あるいはデータベース処理のサポートを受けたいのであれば、PHPで作成し、INTER-Mediatorを読み込んで利用することをお勧めします。スクリプトは、INTER-Mediatorで作ったUIから起動することも可能ですが、cron等を利用してOSの仕組みを利用して一定時間毎に稼働させるということもよく行われるでしょう。なお、UIから起動するには、そのための「アドバイス定義クラス」を作成します。「独立したスクリプト」は、アプリケーションのAPIを作るための手法でもあります。

PHPファイルの置き場所

 サーバーサイドでの拡張処理は、すべてPHP言語で記述します。そのスクリプトは、.php拡張子のファイルに記述します。PHPでは、クラスの定義をする場合にはクラス名に.phpを付与したファイル名にすることが一般的です。独自に作成するPHPファイルをどこに置くのかですが、もっとも単純な手法は、定義ファイルと同じディレクトリーに保存をしておくことで、ほとんどの場合は問題がありません。

 もし、いくつかのフォルダーにまたがる定義ファイル同士で同じ拡張のためのPHPファイルを利用するような、定義ファイルと別の場所に置く必要があるとしたら、次のように対処してください。それぞれの定義ファイルの最初に、INTER-Mediator.phpファイルへの読み込みをrequire_once関数等で行っていますが、その直後に、自分で作成したPHPファイル自身の読み込みを、require_once等で行ってください。パスは、絶対パスでも、相対パスでも構いません。処理の実行前に、確実にクラスをロードするためです。

 PHPでは、php.iniファイルに記述したinclude_pathの値にしたがって、パスを検索します。このパスには、「.」つまりスクリプトを稼働させた時のディレクトリーを必ず探すような設定が既定値なので、定義ファイルと同一のディレクトリーに配置するのが確実な方法です。場合によっては、php.iniファイルのinclude_pathの設定に参照するパスを追加して、作成したPHPファイルへの参照をPHPのシステムレベルで行うようにすることも検討しましょう。ただし、ファイルが数個程度なら、定義ファイルの最初にrequire_onceで読み込んでおくのが手軽です。

PHPでの拡張クラス内でのデータベース処理

 いずれにしてもプログラムの中では、何らかのデータベース処理を行うことが一般的でしょう。INTER-Mediatorではフレームワークを構成するためのAPIを使って、データベース処理を構築していましたが、Ver.9の段階で、シンプルな記述でデータベース処理が可能なトレイト「Proxy_ExtSupport」を開発しましたので、データベース処理はひとつのステートメントでできるようになっています。まず、このデータベース処理拡張トレイトについて説明をしましょう。トレイトを利用する具体的な方法は、この後に説明します。まずは、どのようなメソッドが用意されているのかをまとめます。

 Proxy_ExtSupportトレイトが定義されている名前空間は、INTERMediator\DBです。利用する場合は、useを記述します。結果的に、リスト8-1-1のような記述をクラスの前に記述し、クラスの中のプロパティ等を記述するレベルに、「use Proxy_ExtSupport;」と記述することが一般的かと思われます。

リスト8-1-1 Proxy_ExtSupportトレイトの利用
use INTERMediator\DB\Proxy_ExtSupport;

 Proxy_ExtSupportトレイトのメソッドについては詳細に説明しますが、まずは、単純な利用方法を、リスト8-1-2に記述しておきます。要するに、データベース処理を1ステートメントで実装できるということです。これらは、もちろん、「use Proxy_ExtSupport;」を記述したクラスのメソッド内で出てくる記述になりますが、そのクラスにデータベース処理が「追加」されるので、$thisに対して、Proxy_ExtSupportクラスのメソッドを直接記述できます。

リスト8-1-2 Proxy_ExtSupportのメソッド利用例
// テーブルproductがあり、主キーフィールドがproduct_idであるとする

// 以下のメソッドでproductテーブルの全レコードが配列$resutlに入る
$result = $this->dbRead("product");

// 以下のメソッドでproductテーブルにあるproduct_idフィールドの値が3000から3999までのレコードが配列$resutlに入る。データベースから取り出した結果はproduct_idの昇順で並べ替えられている
$result = $this->dbRead("product",
    [["field" => "product_id", "operator" => ">=", "value" => 3000],
     ["field" => "product_id", "operator" => ">=", "value" => 3000]],
    [["field" => "product_id", "direction" => "asc"]]);

// 以下のメソッドでproductテーブルにあるproduct_idフィールドの値が3665のレコードが配列$resutlに入る
$result = $this->dbRead("product", ["product_id" => 3665]);

// 以下のメソッドでproductテーブルにあるproduct_idフィールドの値が3665のレコードのpriceフィールドが23000に更新され、新たなデータが入ったそのレコードの内容が配列$resutlに入る
$result = $this->dbUpdate("product, ["product_id" => 3665]", ["price" => 23000]);

// 以下のメソッドでproductテーブルにレコードが作成され、product_nameフィールドは「NewOne」、priceフィールドは7000に設定される。そして、新たなデータが入ったそのレコードの内容が配列$resutlに入る
$result = $this->dbCreate("product", ["product_name" => "NewOne", "price" => 7000]);

 メソッドの利用例のプログラムを解説します。概して、db*という名前のメソッドがあって、最初の引数にコンテキストを指定しますが、テーブル名を直接指定したいと思うような場合がほとんどでしょう。最初の引数にテーブル名を指定した時、それ以外に特に何もしなければ、主キーフィールド名は「テーブル名_id」であると仮定して処理を進めます。そうでない主キーフィールドの場合の対処は後で説明します。そして、検索条件や並べ替えのルールは、定義ファイルに記述するのと同じ記述方法が使えます。なお、検索条件については、演算子は=と固定になりますが、「フィールド名 => 値」の形式での指定もできるようになっています。処理が成功すれば、何かしらデータを返しますが、失敗したらfalsyな値を返します。通常、返り値が真かどうか、つまり「if (!$result) {エラー処理}」的なプログラムにより、エラーに対する対処の記述が可能です。以下、これらのメソッドについて解説しておきます。

$this->dbRead($target, $query = null, $sort = null, $spec = null)

コンテキストに対して、指定した検索条件、並べ替えるルールに従ってデータを取り出して配列で返す。

$this->'dbUpdate($target, $query = null, $data = null, $spec = null)

コンテキストに対して、指定した検索条件のレコードのフィールドの値を更新し、そのレコードのデータを配列で返す。

$this->dbCreate($target, $data = null, $spec = null)

コンテキストに対してレコードを新規作成して指定されたフィールドの値を設定し、そのレコードのデータを配列で返す。

$this->dbDelete($target, $query = null, $spec = null)

コンテキストに対して、指定した検索条件に合致したレコードを削除する。成功するとtrueが返る。

返り値/引数設定内容
返り値処理が正しく行われれば対象のレコードが、「フィールド名 => 値」の形式の連想配列を1レコードとして、その配列で得られる。なお、dbDeleteは論理値が返る。処理に失敗すれば、falseが返る
$target事前にdbInitメソッドを利用したり第4引数の指定がある場合、利用するコンテキスト名。もし、コンテキスト定義がない場合は対象のテーブル名と解釈する
$query検索条件を指定する。定義ファイルのコンテキスト定義にあるqueryキーの配列と同様な形式に加えて、「フィールド名 => 値」の形式での指定も可能で、その場合は演算子は=固定となる
$sort並べ替えのルールを指定する。定義ファイルのコンテキスト定義にあるsortキーの配列と同様な形式に加えて、「フィールド名 =>"(ASC|DESK)"」形式でも指定が可能
$data更新するデータ、新規レコードのフィールドに設定するデータを、「フィールド名 => 値」の配列で指定する(なお、他のAPIと同様に["field" => "lastDT", "value" => $nowDT]形式の配列の配列で指定は可能だが、「フィールド名 => 値」は同等でありよりシンプルである)
$specコンテキスト定義等を指定する(別途、解説あり)
表8-1-1 db*メソッドの返り値と引数

 前の例では、主キーフィールドに自由度がないと感じるかと思われます。その場合は、dbReadメソッド等を呼び出す前に、setFixedKeyメソッドを呼び出します。このメソッドを使う場合は、原則として、db*メソッドの前に毎回このメソッドを呼び出すことを基本とします。

$this->setFixedKey($key)

コンテキストに対するkeyキーに対する値を引数に指定したものに固定する。

 例えば、INTER-Mediatorの認証システムでは、ユーザをauthuserテーブルに記録しています。このauthuserテーブルに対して、コンテキストを指定しないで中身を全部読み出すのであれば、例えば、リスト8-1-3ようなプログラムになります。

リスト8-1-3 setFixedKeyメソッドを利用する場合
$this->setFixedKey('id');
$result = $this->dbRead('authuser', null, ['realname' => 'ASC']);

 コンテキストを定義してデータベース処理をしたいという場合には、最初にdbInitメソッドで定義をしておく方法と、db*メソッドの$spec引数にそのメソッドの中だけでコンテキスト定義を指定する方法があります。リスト8-1-4に例を示します。dbInitを使うとしたら、一連のプログラムにいくつもコンテキスト定義をするのなら、最初にひとつにまとめて行ってそれぞれ名前だけで指定できるというところのメリットがあるでしょう。後半の例は、テーブル名を使いたいけど、主キーフィールド名が「テーブル名_id」のルールに従っていないような場合の対処とも言えます。いずれにしても、INTER-Mediatorに精通した方なら、コンテキストを使って定義することはそれほど難しいことではないと思われるので、さまざまな設定をどこで行うかという選択肢のひとつとして、PHPのプログラムでもコンテキストの定義が使えることを知っておいて損はないでしょう。

$this->dbInit($datasource = null, $options = null, $dbspec = null, $debug = null)

定義ファイルで使うIM_Entryと同様な引数を指定する。コンテキストをまとめて定義するという意味があり、定義したコンテキストは再定義するまで生きている。返り値はなし。

リスト8-1-4 コンテキストを記述してデータベース処理をさせたい場合
$this->// これ以降で、testtable_procというコンテキストが利用できる
$this->dbInit([
    ['name' => "testtable_proc",
     'view' => "testtable_adding",
     'table' => "testtable",
     'key' => 'id']
    ]);
$result = $this->dbCreate("testtable_proc")

// testtableテーブルにレコードを作成するが、その時の設定は、3つ目の$spec引数に指定したものが利用される
$result = $this->dbCreate("testtable",
    ['vc1' => 'a', 'vc2' => 'b',],
    [['name' => "testtable", 'key' => 'id']]);

 PDOのデータベースを利用する場合には、トランザクションにも対応しています。以下のメソッドを利用できます。

$this->beginTransaction()

トランザクションを開始する

$this->rollbackTransaction()

トランザクションを終了し、開始前の状態に戻す

$this->commitTransaction()

トランザクションを終了し、ここまでのデータベース処理を実際に記録する

8-2アドバイス定義クラスの作成

このセクションでは、アドバイス定義クラスについての説明と、利用例を示します。PHP言語によるサーバー拡張を扱ったサンプルの解説を行うため、定義ファイルとページファイルというエディターを用意しているファイルだけでなく、拡張のプログラムファイルも作成する必要があります。そのため、VMでの開発作業はPHP経験者でないと難しい面もあるかと思います。一方、PHP経験者の方なら、結果だけを見ることで、開発方法の実例になると思われます。以上の理由で演習手順ではなくサンプルとしての解説を行います。

アドバイス定義クラスの作成

 「アドバイス」とは、アスペクト指向プログラミングにおける振る舞いを示す用語です。オブジェクト指向にアスペクト指向を適用した場合、複数のクラスに対して横断的な仕組みが存在する場合、それを「アスペクト」として認識します。アスペクトとして定義されたモジュールには、アドバイス(例えばメソッド)として実装されるものがあります。そして、クラスの中では、「ジョイントポイント」としてアドバイスを織り込む(「ウィーブする」と表現される)位置を指定します。この手法では、つまりは既存のクラスの中で、特定の振る舞いを注入することになります。もちろん、INTER-Mediatorはオブジェクト指向的な開発環境ではなく、アスペクト指向的な要素も薄いのですが、こうした動作に近い動きをする拡張機能を「アドバイス定義クラス」と命名しました。

 アドバイス定義クラスは、簡単に言えば、データベース処理の前後に、メソッドの実行ができるものです。その意味では、フィルタ的な動作と思っていただいて構いません。データベース処理の前後に追加されるでは、Proxy_ExtSupportトレイトにより、データベースへの処理が記述可能です。また、検索条件等はもちろん、JavaScriptで追加した条件や、あるいはコンテキストに定義されたさまざまな設定、更新や新規レコード時のフィールドのデータの取得や設定などもできます。INTER-Mediatorの標準機能だけでは実現できないような複雑なデータ処理を記述する、すなわちロジックを追加するような用途にアドバイス定義クラスは利用できます。

 前処理では、コンテキストで定義したデータベース処理を行う前であり、実際に行うかどうかの判定が複雑な場合での対処も記述できます。前処理の返り値に応じて、コンテキスト定義の処理をキャンセルすることも可能です。後処理では、コンテキスト定義に従ったデータベース処理の結果を、レコードを連想配列とした配列で得られます。その配列を自由に変更して構いませんが、何らかの同一形式、つまり、連想配列の配列を返り値として返す必要があります。その配列が、クライアントに送り届けられます。SQLデータベースの場合は集計処理を記述する方法もありますが、場合によっては生データを取り出して後処理で集計処理をするということも考えられます。

アドバイス定義クラスで利用するインターフェース

 アドバイス定義クラスで利用するインターフェースを以下のように定義しています。このインターフェースを使わなくてもメソッド呼び出しは行われますが、インターフェースを利用することで、メソッドの定義段階で、名前がちょっと間違っていたと言ったようなことが開発ツールによって検出できるので、インターフェースを使うことを基本とします。インターフェース定義はリスト8-2-1に示し、クラスの定義例は、その後に紹介します。

リスト8-2-1 アドバイス定義クラスで利用できるインターフェース
//データベースから読み出す処理の前処理のためのメソッド
interface INTERMediator\DB\Extending\BeforeRead {
    public function doBeforeReadFromDB();
}

//データベースから読み出す処理の後処理のためのメソッド
interface INTERMediator\DB\Extending\AfterRead {
    public function doAfterReadFromDB($result);
}

//データベースから読み出す処理の後処理のためのメソッド(ページネーションを行うとき)
interface INTERMediator\DB\Extending\AfterRead_WithNavigation {
    public function doAfterReadFromDB($result);
    public function countQueryResult();
    public function getTotalCount();
}

//データベースを更新する処理の前処理のためのメソッド
interface INTERMediator\DB\Extending\BeforeUpdate {
    public function doBeforeUpdateDB();
}

//データベースを更新する処理の後処理のためのメソッド
interface INTERMediator\DB\Extending\AfterUpdate {
    public function doAfterUpdateToDB($result);
}

//データベースに新規レコードを作成する処理の前処理のためのメソッド
interface INTERMediator\DB\Extending\BeforeCreate {
    public function doBeforeCreateToDB();
}

//データベースに新規レコードを作成する処理の後処理のためのメソッド
interface INTERMediator\DB\Extending\AfterCreate {
    public function doAfterCreateToDB($result);
}

//データベースからレコード削除する処理の前処理のためのメソッド
interface INTERMediator\DB\Extending\BeforeDelete {
    public function doBeforeDeleteFromDB();
}

//データベースからレコード削除する処理の後処理のためのメソッド
interface INTERMediator\DB\Extending\AfterDelete {
    public function doAfterDeleteFromDB($result);
}

//データベースでレコードの複製処理を行う前処理のためのメソッド
interface INTERMediator\DB\Extending\BeforeCopy {
    public function doBeforeCopyInDB();
}

//データベースでレコードの複製処理を行った後処理のためのメソッド
interface INTERMediator\DB\Extending\AfterCopy {
    public function doAfterCopyInDB($result);
}

コンテキスト定義でクラス名を指定する

 基本的には、いずれのインターフェースも、メソッドがひとつだけです。ただし、Extending_Interface_AfterRead_WithNavigationについては3つのメソッドの実装が必要になります。アドバイス定義クラスで、レコード数が変わってしまうような処理をした場合、そのコンテキストをページネーションに結びつけていると、ページネーションのコントールに対して、アドバイス定義クラスによる変更前の現在のレコード数や全レコード数が供給されます。これらの値を、アドバイス定義クラスのメソッドで修正した後の現在のレコード数や全レコード数をクライアントに供給できるように、それらの結果を返すメソッドも実装してください。

 ここであるコンテキスト「salesitems」に対して、前処理と後処理を追加したいとします。その処理を記述するクラス名は、自分で命名します。もちろん、PHPのクラス名になり得るものを指定します。ここでは、「AdditionalProccess」とします。このクラス名は、コンテキスト定義にextending-classキーで記述する必要があります。したがって、定義ファイルでのコンテキスト定義は、例えば、リスト8-2-2のようになります。

リスト8-2-2 アドバイス定義クラス名を指定したコンテキストの例
<?php
require_once("pathTo/INTER-Mediator.php");

IM_Entry(
    array(
        array(
            "name" => "salesitems",
            "view" => "items",
            "query" => array(array("field" => "year", "operator" => "=", "value" => "2016"),),
            "extending-class" => "AdditionalProccess",
        ),

アドバイス定義クラスのPHPでの定義

 そして、アドバイス定義クラスを、定義ファイルと同一のディレクトリーに作ります。ファイル名は、クラス名に.phpをつけた「AdditionalProccess.php」にします。こうすれば、ファイルを自動的に検出できるようにINTER-Mediatorではクラスローダーが機能します。したがって、アドバイス定義クラスを定義ファイル内でrequre_once等で読み出す必要は通常はないと思われます。AdditionalProccessクラスは、リスト8-2-3のように定義します。まず、classによってクラスを定義するとき、implementsで使用するメソッドのインターフェースを列挙します。また、UseSharedObjectsクラスを継承します。そして、それらのインターフェースで定義されている2つのメソッドを実装します。ここでは、サンプルなので、何も処理をしないものを記述しますが、実際にはメソッド内にはさまざまな記述がなされるでしょう。データベース処理前後に行いたい作業を、それぞれのメソッドに記述します。afterで始まる後処理のメソッドでは、$resultには検索結果のレコードが、連想配列の配列で返されます。アドバイス定義クラスがないときと同一の動きをさせるには、$result変数をreturnで返すだけでOKです。なお、これらのメソッド内でデータベース処理をすることが一般的でしょうから、Proxy_ExtSupportトレイトもクラス内部でuseで宣言しておきます。

リスト8-2-3 アドバイス定義クラスの作成例
<?php

use INTERMediator\DB\UseSharedObjects;
use INTERMediator\DB\Proxy_ExtSupport;
use INTERMediator\DB\Extending\BeforeRead;
use INTERMediator\DB\Extending\AfterRead;

class AdditionalProccess extends UseSharedObjects
    implements BeforeRead, AfterRead
{
    use Proxy_ExtSupport;
    
    public function doBeforeReadFromDB()  {
    }

    public function doAfterReadFromDB($result)  {
         return $result;
    }
}

 beforeで始まる前処理のメソッドは、返り値に応じてエラー処理などができるようになっています。返り値がない場合と""が返された場合は、エラーがなかったものとしてそのままINTER-Mediator側のデータベース処理に移行します。一方、""ではない文字列が返された場合、その文字列を警告として記録するとともに、エラーが発生したと解釈して、INTER-Mediatorのデータベース処理には移行しません。また、falseが返された場合もエラーとしてその先の処理をキャンセルします。もちろん、これらの場合は後処理も行いません。ブラウザ側では、その後に返された文字列がダイアログボックスで表示されるのが一般的なインターフェースです。

INTER-Mediatorの内部クラスとデータベース処理

 ここで、INTER-Mediatorの内部でデータベース処理をしているクラスの概要を説明します。その知識をもとに、アドバイス定義クラスでのプログラミングが必要になる場合があると想定されるからです。

 INTER-Mediator内部のデータベース処理はかなり複雑ですが、階層化はされています。中心的なものは、データベース利用のアーキテクチャごとに用意されたクラスで、ここでは「データベースクラス」と呼ぶことにします。総称としてはINTERMediator\DB\DBClassが抽象クラスとして定義されています。具体的なクラスは表8-2-1のようなクラスが定義されています。この種のクラスを新たに定義するという拡張も考えられますが、かなり多数のメソッドの実装が必要なので現実的ではないでしょう。むしろ、そういうことになった場合は、INTER-Mediator内部で新たなデータベースクラスの定義をすべきです。

プロパティ名dbClassキーでの指定値利用アーキテクチャ
INTERMediator\DB\PDOPDOPHPのPDO
INTERMediator\DB\FileMaker_FXFileMaker_FXFX.phpつまりFileMaker CWPのXML共有
INTERMediator\DB\FileMaker_DataAPIFileMaker_DataAPIFileMaker Data API
INTERMediator\DB\TextFileTextFileテキストファイル(ただし読み出し処理のみ)
INTERMediator\DB\NullDBNullDBs(何もしない)
表8-2-1 INTER-Mediatorで定義されたデータベースクラス

 内部ではさらに、データベースエンジンごとに異なる処理を実装するための仕組みもありますが、アプリケーション作成においては特にそこまで調べる必要がないので、本書では省略します。

 データベースクラス以外に、INTER-Mediatorでは、INTERMediator\DB\Proxyというクラスが用意されています。クライアントからのリクエストは、Proxyが受け取り、使用するデータベースに応じてデータベースクラスに引き渡される形になります。Proxyではパラメータの処理や認証処理の主要部分などが組み込まれています。以前のバージョンでは、独自にデータベース処理を行う場合にはProxyを生成してメソッドを呼び出すことをしていましたが、Proxy_ExtSupportトレイトがあるので、現状ではProxyクラスのインスタンスを生成する必要はまずないと考えます。なお、Proxy_ExtSupportトレイトは独自にProxyを生成するので、アドバイス定義クラス内部でのみ使うProxyが背後で自動的に生成されていると考えてください。どうしてもアドバイス定義クラスでProxyを使いたいという方は、自分で生成ということもありますが、以下のように、Proxy_ExtSupportトレイトで生成したProxyの取得もできるようにしました。

$this->getExtProxy()

Proxy_ExtSupportトレイト内部で処理するために生成したProxyオブジェクトのインスタンスを取得する。

 アドバイス定義クラスを定義するときにUseSharedObjectsを継承すると、INTER-Mediator内部のさまざまな情報が取得できます。表8-2-2に示すようなプロパティ参照を利用して、データベースクラス、Logger、Settingsクラスのオブジェクトへの参照を、アドバイス定義クラス内からできるようになります。クエリーを行うときにコンテキストなどで指定した検索条件を取り出すなどの作業は、後述するSettingsクラスのメソッドで行えます。

プロパティ名クラス内からの参照クラス
dbClass$this->dbClassINTERMediator\DB\*
logger$this->loggerINTERMediator\DB\Logger
dbSettings$this->dbSettingsINTERMediator\DB\Settings
表8-2-2 DB_Proxyクラスやアドバイス定義クラスで利用できるプロパティ

 アドバイス定義クラスの定義において、UseSharedObjectsクラスを継承しなくてもかまいませんが、その時はデータベースの処理結果を配列でもらって処理する程度しかできず、定義ファイルの設定やクライアントのJavaScriptで追加された検索条件等へのアクセスはやりにくくなります。これらは、Settingsクラスのメソッドで取得できるので、さまざまな処理を組み込みやすくするためにもUseSharedObjectsクラスを継承しておくのが原則と考えてよいでしょう。

 サーバサイドで利用可能なメソッドのうち、アプリケーション開発でよく利用されるものを本書では紹介していますが、より多くのAPIを知りたい方は、マニュアルの『サーバーサイドの拡張』を参照してください。もちろん、ソースコードも公開しているので、ソースを読むのも情報をえるひとつの手段です。

ログ作成の機能

 エラーメッセージやデバッグメッセージは、サーバー側ではINTERMediator\DB\Loggerクラスで管理しています。シングルトンで管理しているため、Proxyを借りに複数利用したとしても、ログ自体は一本化されており、処理終了後にまとめてブラウザのコンソール等で確認ができるようになっています。以下のメソッドを使うことで、追加や参照が可能です。処理の途中でエラーがあるかどうかについては、getErrorMessagesメソッドが返す配列の要素数をcount関数で調べることで分かります。もちろん、$this->loggerという参照方法もありますが、シングルトンなのインスタンスを取得して処理をしても構いません。

Logger::getInstance()

Loggerクラスのシングルトンインスタンスを取得する。

Logger->setDebugMessage($str, $level)

引数$strに指定した文字列を、引数$levelに指定したレベルでのデバッグメッセージとして記録する。レベルは1ないしは2のみをサポートし、引数$levelを省略すると1になる。

Logger->setWarningMessage($str)

引数$strに指定した文字列を、警告メッセージとして記録する。

Logger->setErrorMessage($str)

引数$strに指定した文字列を、エラーメッセージとして記録する。

Logger->getDebugMessages()

記録されたデバッグメッセージを要素として含む配列を返す。

Logger->getErrorMessages()

記録された警告メッセージを要素として含む配列を返す。

Logger->getErrorMessages()

記録されたエラーメッセージを要素として含む配列を返す。

Logger->getErrorMessages()

記録されたエラー/警告/デバッグメッセージのいずれもクリアする。

コンテキストの指定と基本情報取得のAPI

 コンテキストおよびそれに関連したさまざまな設定のためのAPIがSettingsクラスとして利用可能です。アドバイス定義クラスを作成する場合、以下に示すメソッドのうちゲッターメソッドを使うことがほとんどかと思われます。

Settings->getDataSourceDefinition($dataSourceName)

引数に指定した文字列をnameキーの値として持つコンテキスト定義の連想配列を返す。

Settings->getDataSourceTargetArray()

現在、選択されているコンテキストの定義内容を連想配列で返す。

Settings->getDataSourceName()

現在、選択されているコンテキストのnameキーに対する値。

Settings->getEntityForRetrieve()

クエリー処理に利用するエンティティ名を返す。つまり、viewキーの値が指定されていればその値、指定されていない場合にはnameキーの値が返される。

Settings->getEntityForUpdate()

更新処理に利用するエンティティ名を返す。つまり、tableキーの値が指定されていればその値、指定されていない場合にはnameキーの値が返される。

Settings->setStart($st)

検索結果の最初のいくつ目から結果として取り出すかを、引数の数値で指定する。クライアントのINTERMediator.startFromの値が自動的に設定される。

Settings->getStart()

検索結果の最初のいくつ目から結果として取り出すかが得られる。

Settings->setRecordCount($sk)

検索結果の中から、最大でいくつのレコードを取り出すかを引数の数値で指定する。コンテキストのrecordsキーの値や、クライアントINTERMediator.pageSizeの値など、すでに決まっている値が指定される。

Settings->getRecordCount()

検索結果の中から、最大でいくつのレコードを取り出すかが得られる。

IM_Entry関数の呼び出し引数の設定と取り出し

 アドバイス定義関数の中で、定義ファイルに指定した内容をそのまま利用したいときのために、以下のようなそれぞれの引数の記録および取り出しのメソッドを定義しています。

Settings->getDataSource()

定義ファイルのIM_Entry関数の第1引数の値が返される。

Settings->getOptions()

定義ファイルのIM_Entry関数の第2引数の値が返される。

Settings->getDbSpec()

定義ファイルのIM_Entry関数の第3引数の値が返される。

検索条件や設定値などフィールドと値に関するAPI

 Settingsには「フィールドの配列」と「値の配列」を保持するプロパティをそれぞれ持ちます。フィールドと値が対になる場合には要素数はそれぞれ同数になりますが、フィールドだけを指定した処理も想定して連想配列としないで、別々の配列を用意しています。その配列への設定や取り出しのメソッドが以下のように用意されています。

 オペレーションが「read」の場合は、リピーター内にあるターゲット指定のフィールドがフィールドの配列に設定され、値の配列には何も設定されません。オペレーションが「update」の場合は、更新するフィールドとその値が、それぞれフィールドの配列と値の配列に設定されます。オペレーションが「delete」の場合はフィールドの配列も値の配列も使用しません。オペレーションが「create」の時は、新しいレコードの初期値の指定をフィールドの配列と値の配列で設定されます。通常は、コンテキストのdefault-valuesキーの値と、INTERMediator.additionalFieldValueOnNewRecordプロパティの値の両方が、フィールドの配列と値の配列に設定されます。オペレーションが「copy」の場合にはフィールドの配列も値の配列も使われません。

 これらのメソッドは、アドバイス定義クラスでのデータベース処理前に呼び出されるメソッド、例えばレコード作成の場合のdoBeforeCreateToDBメソッド等で利用できます。新規作成されたレコードのフィールドの値を、PHPのプログラムで指定できるので、単にユーザーインターフェースから入力された値を初期値として利用するだけでなく、Web APIを呼び出して得られた値をあるフィールドの初期値として指定するようなことも可能です。

Settings->setFieldsRequired($fieldsRequired)

フィールドの配列として、引数の配列を設定する。このメソッドは、配列そのものを設定するが、addValueWithField、addTargetFieldメソッドにより、フィールド一覧を管理する配列へ要素が追加される。

Settings->getFieldsRequired()

フィールドの配列を返す。

Settings->addTargetField($field)

フィールドの配列の要素として、引数に指定した文字列を追加する。

Settings->getFieldOfIndex($ix)

フィールドの配列から、引数に指定した番号の要素を返す。

Settings->setValue($values)

値の配列として、引数の配列を設定する。

Settings->getValue()

値の配列を返す。

Settings->addValue($value)

値の配列の要素として、引数に指定した文字列を追加する。

Settings->addValueWithField($field, $value)

フィールドの配列および値の配列の要素として、引数に指定した文字列をそれぞれ追加する。

Settings->getValuesWithFields()

フィールドの配列にある値をキー、そのキーに対する値を要素にした連想配列を返す。

Settings->getValueOfField($targetField)

引数に指定したフィールド名をフィールドの配列の何番目なのかを判別し、値の配列の同じ番号の要素を返す。つまり、フィールド名に対応した値を返す。

 以下のメソッドは、Settingsオブジェクトが持つ「外部キー値の配列」を利用するためのものです。この外部キー値の配列は、オペレーションがreadの時にだけ使用され、親子関係にあるエンクロージャー/リピーターのセットを発見したとき、子のエンクロージャーに対するデータベースアクセス時において、その外部キーに対応する親のリピーターにあるフィールド名と値を記録します。

Settings->setForeignFieldAndValue($foreignFieldAndValue)

引数を外部キーの値を保持する配列に指定する。引数は、field、valueをキーとした連想配列の配列である必要がある。

Settings->getForeignFieldAndValue()

外部キーの値を保持する配列を返す。返される値は、field、valueをキーとした連想配列の配列。

Settings->addForeignValue($field, $value)

引数に指定したフィールド名と値を、外部キーの値を保持する配列に追加する。

Settings->getForeignKeysValue($targetField)

外部キーの値を保持する配列から、引数に指定したフィールドに対する値を返す。ない場合はnullが返る。

 以下のメソッドは、Settingsオブジェクトが持つ「検索条件の配列」に対するものです。createを除くオペレーションでは、コンテキストのqueryキーによる配列の設定が必ず適用されます。これらは、検索条件の配列には入力されず、データベースクラスが実際のデータベース処理を行うときにコンテキスト定義から取り出して検索条件として設定されます。オペレーションがreadの場合は、INTERMediator.additionalConditionプロパティの内容がこの配列に設定されます。オペレーションがreadやupdate、delete、copyの場合は、JavaScriptのAPIで追加した検索条件もこの配列に設定されます。オペレーションがcreateの場合はこの設定は利用されません。

Settings->addExtraCriteria($field, $operator, $value)

追加的な検索条件を保持する配列に、引数の3つの要素を持つ連想配列として追加する。

Settings->getExtraCriteria()

追加的な検索条件を保持する配列を返す。

Settings->unsetExtraCriteria($index)

追加的な検索条件を保持する配列の中にある引数に指定したインデックスの要素を削除する。

Settings->getCriteriaValue($targetField)

追加的な検索条件を保持する配列から、引数に指定した文字列をfieldキーの値として持つ最初の要素を特定し、その要素のvalueキーの値を返す。

Settings->getCriteriaOperator($targetField)

追加的な検索条件を保持する配列から、引数に指定した文字列をfieldキーの値として持つ最初の要素を特定し、その要素のoperatorキーの値を返す。

 FileMaker Serverを使う場合に、データベース処理前にグローバルフィールドの設定をするとき、コンテキストのglobalキーを使用できますが、その値は実行時に動的に設定することができません。そこで、INTERMediator.additionalConditionプロパティに指定した上でアドバイス定義クラスのデータベース処理前のメソッドにおいて、getCriteriaValueを利用して検索条件から値を取り出し、setGlobalInContextでグローバルフィールドの設定に値を追加します。unsetExtraCriteriaを利用して追加の検索条件から設定値を取り除く必要があります。

Settings->setGlobalInContext($contextName, $operation, $field, $value)

引数に指定したコンテキストに、残りの引数で指定した設定内容を持つglobalキーの連想配列を追加する。もちろん、FileMaker Serverのみで意味のある機能である。

 楽観的ロックにおいては、レコードの特定を主キーだけを使い、他の条件を無視する必要があるかもしれません。他のユーザーがフィールドの値を変更して検索条件に合わない状態になっているとします。コンテキストの検索条件をすべて適用すると、レコードがヒットせず、今現在のフィールドの値を取得できません。そのために、検索においては主キーのみを使うという動作をサポートしています。通常は自動的に設定されますが、以下のメソッドで意図的に設定したり、あるいは現在の設定が分かります。

Settings->setPrimaryKeyOnly($primaryKeyOnly)
Settings->getPrimaryKeyOnly()

検索条件の中から、主キー(コンテキストのkeyキー)で指定されたものだけを利用する設定とその状態の取得。なお、主キーのみを利用する検索は、データベースの更新前に楽観的ロックの仕組みを利用して、現在の値を取り出す場合に利用している。

 データベースへのオペレーションがreadのとき、INTERMediator.additionalSortKeyプロパティに指定したソートフィールドの昇順/降順の設定は以下のメソッドを使用して、Settingsオブジェクトに記録されます。なお、コンテキストに指定したsortキーによるソート条件は、データベースクラスで適用されます。値を得たい場合は、コンテキストの定義を取り出して読み出せばよいでしょう。

Settings->addExtraSortKey($field, $direction)

追加のソート条件を記録した配列に、引数にしていたフィールドと基準(昇順ないしは降順)を追加する。

Settings->getExtraSortKey()

追加のソート条件を記録した配列を得る。

 オペレーションがcopyの時で、コピーするレコードを含むコンテキストに対してrelationキーによって関連レコードが定義されているときに関連レコードも含めてコピーしたいときがあります。その場合は、関連レコードのコンテキストと検索条件を以下のAPIで記録します。

Settings->addAssociated($name, $field, $value)

レコードのコピーにおいて、関連するコンテキストに対する設定を追加する。

Settings->getAssociated()

レコードのコピーにおいて使用される関連するコンテキストに対する配列を得る。

データベース設定に関連するAPI

 以下のSettingsクラスのメソッドは、データベース接続の設定、すなわちIM_Entry関数の3つ目の引数の配列に指定するデータの設定と取得ができるものです。メソッド名と配列のキーを対応付けているので、メソッド名のみの紹介で意味は理解できるので、メソッド名の紹介のみとします。ここで、ゲッターメソッドは、IM_Entryの引数の値ではなく、もし、params.phpファイル、あるいは現在のコンテキストに指定がある場合に、その値が設定されます。つまり、実際にデータベース処理で使われるサーバーのホスト名やデータベース名がゲッターから得られることになります。

Settings->setDbSpecServer($str)
Settings->getDbSpecServer()
Settings->setDbSpecPort($str)
Settings->getDbSpecPort()
Settings->setDbSpecUser($str)
Settings->getDbSpecUser()
Settings->setDbSpecPassword($str)
Settings->getDbSpecPassword()
Settings->setDbSpecDataType($str)
Settings->getDbSpecDataType()
Settings->setDbSpecDatabase($str)
Settings->getDbSpecDatabase()
Settings->setDbSpecProtocol($str)
Settings->getDbSpecProtocol()
Settings->setDbSpecDSN($str)
Settings->getDbSpecDSN()
Settings->setDbSpecOption($str)
Settings->getDbSpecOption()

データベース処理に関する設定を行ったり取り出したりするメソッド。

認証やアクセス権に関わるAPI

 ログインしているユーザー名を得たり、認証に関するさまざまな設定を取得したい場合には、以下のメソッドを利用します。

Settings->getCurrentUser()

クライアントから申告されたユーザー名を取得する。アドバイス定義クラスの処理を進める段階ではすでに認証の処理が行われており、ユーザ名とチャレンジが違っていれば、認証は成立しないので、チャレンジを送付したユーザと考えて良い。

Settings->getAuthenticationItem($key)

IM_Entry関数の第2引数(オプション設定)のauthenticationキーに対する値に対し、さらに引数の文字列のキーの値を取り出す。もし、引数に与えたキーに対する値が定義されていない場合で、引数がテーブル名の場合には、規定のテーブル名を返す。あるいは認証継続時間の場合には既定値として8時間が返される。

[利用例] レコードを作成時に別のテーブルにも関連レコードを作成する

 一般的な伝票では、伝票と明細が1対多の関係になっています。「伝票を作成するけど明細は作成しない」ということはまずなく、普通はひとつ以上の明細が存在します。であれば、伝票を作成時に明細をひとつ、あるいは複数個作成してしまうということが考えられます。こうした処理はアドバイス定義クラスで明細テーブル側のレコードを作成することができます。

 まず、テーブル構成ですが、伝票側をaccountテーブル、明細側をdetailテーブルとして、自動連番が入力されるフィールドがそれぞれaccount_idとdetail_idがあり、いずれもサロゲートキーによる主キーとなっています。それぞれが1対多の関係にするために、detail側には、外部キーとして対応するaccountテーブルのレコードを特定するためのaccount_idフィールドが用意されています。伝票の一覧があり、リスト8-2-4のようにコンテキストが定義されているとします。ここでは、repeat-controlでconfirm-insertが設定されているので、ページ上のページネーションの部分にbutton-namesで定義した「新規会計項目」ボタンがあり、これをクリックすることで、accountテーブルにレコードが作成されるものとします。加えて、extending-class設定にCreateFirstItemが指定されています。

リスト8-2-4 アドバイス定義クラスを利用したコンテキスト定義
require_once './vendor/inter-mediator/inter-mediator/INTER-Mediator.php';

IM_Entry(
    [
        [
            'name' => 'account_list',
            'table' => 'account',
            'key' => 'account_id',
            'default-values' => [....],
            'repeat-control' => 'confirm-insert confirm-delete confirm-copy',
            'button-names' => ['insert' => '新規会計項目作成'],
            'calculation' => [....],
            'numeric-fields' => ['item_total'],
            'extending-class' => 'CreateFirstItem',
            :
        ], ... 

 CreateFirstItemクラスは、定義ファイルと同じディレクトリ階層に、CreateFirstItem.phpという名前で作成しました。内容はリスト8-2-5のようになっていますが、AfterCreateインターフェースを実装しているように、レコード作成時に呼び出されます。レコードが作成された後にdoAfterCreateToDBメソッドが呼び出され、引数$resultには作成されたレコードが入力されています。通常は1レコードだけが作られるので、$recordは、フィールド名と値の連想配列がひとつだけ入っている配列になります。メソッド内では、dbCreateメソッドで、detailテーブルにレコードを作成しています。第2引数の配列で、新規作成されるレコードのフィールドの値を指定していますが、ここでは外部キーのaccount_idに、作成されたレコードの配列$recordから値を取り出して設定しています。この入力がないと、detail側に関連するレコードとしてレコードは作成されません。unit_priceとqtyは0が設定されており、ここでレコードの初期値が記述できることがわかります。引数をそのまま返しています。ここでは、このメソッドが呼び出された時には失敗しない前提となっていますが、$recordの要素数が1であるかどうかなどの検証は入れたほうが良いでしょう。なお、UseSharedObjectsを継承していますが、それに関係する仕組みはここでは使っていないので、継承は記述しなくても大丈夫です。

リスト8-2-5 関連テーブルにレコードを作成するアドバイス定義クラス
use \INTERMediator\DB\UseSharedObjects;
use \INTERMediator\DB\Extending\AfterCreate;
use INTERMediator\DB\Proxy_ExtSupport;

class CreateFirstItem extends UseSharedObjects implements AfterCreate
{
    use Proxy_ExtSupport;

    public function doAfterCreateToDB($result)
    {
        $this->dbCreate('detail',
            ['account_id' => $result[0]['account_id'], "unit_price" => 0, "qty" => 0,]);
        return $result;
    }
}

[利用例] アドバイス定義クラスで集計処理を行う

 アドバイス定義クラスは、データベース処理の前後に割り込むことで、サーバー側の動作をカスタマイズすることができます。もっとも理解しやすい例は、コンテキストで指定したレコードを取り出した後、そのレコード群をもとに集計処理を組み込むといったことです。しかしながら、MySQLなどのPDOを使ったデータベースについては、INTER-Mediator Ver.5.3より、aggregaton-selectキー等で、コンテキストに集計を行うSQLの生成を行うための定義を追加することができるようになりましたので、SQLで記述可能な集計はむしろそちらを利用した方がパフォーマンスが高くなります。一方、FileMaker Serverの場合、レイアウトによる集計機能はありますが、小計の機能をカスタムWebアクセス側では利用できないなどの制約があるので、集計やレコード間を跨ぐようなデータ処理をした結果をページに出したいような場合には、アドバイス定義クラスで検索後のデータを変更するプログラムを記述したほうが良い場合もあります。

 サンプルプログラムのひとつで、アドバイス定義クラスを利用しています。アドバイス定義クラスにより集計処理をしているコンテキストもありますが、さらにレコードの生成をアドバイス定義クラスで行うといったことも行っています。演習環境を利用している場合には、ブラウザーで「http://localhost:9080」に接続し、そこにある「サンプルプログラム」のリンクをクリックして、サンプルプログラムの一覧を表示します。そして、「Server-side Extension」の行の「show(setting the class)」をクリックすると、図8-2-1のようにデータを集計したページが表示されます。

図8-2-1 アドバイス定義クラスを使ったサンプルプログラムの動作結果

 このサンプルのページファイルはこちら、定義ファイルはこちらから参照できます。ページファイルは、TABLEタグによる表の中に別のTABLEタグの表がある形式になっています。外側はeverymonthという名前のコンテキストで、内側は、summary1、summary2、dataというコンテキストが展開されていることを、ページファイルよりまずは読み取ってください。

 リスト8-2-6は定義ファイルにあるeverymonthコンテキストの定義内容です。このコンテキストにより、連続した年月日のレコードを作成しています。viewキーによりitem_masterテーブルから検索を行っていますが、queryによる検索条件やrecordsによるレコード数は、単になるべく短く、しかしながらエラーなく検索が行われるようにするための設定であり、取り出すデータとは関係ありません。ここでのポイントになるのは、extending-classキーによってYearMonthGenクラスを指定していることです。

リスト8-2-6 everymonthコンテキストの定義
array(
    'name' => 'everymonth',
    'view' => 'item_master',
    'query' => array(array('field' => 'id', 'operator' => '=', 'value' => '1'),),
    'records' => 1,
    'extending-class' => "YearMonthGen",
),

 同じフォルダーに、YearMonthGen.phpというクラス名と同じファイル名の.phpファイルがあります。リスト8-2-7がそのクラス定義の部分です(ファイル全体はこちらから参照できます)。アドバイスはデータベースの読み出し後に処理をするので、クラスにはインターフェースのExtending_Interface_AfterReadをインプリメントしておき、doAfterReadFromDBを定義します。引数の$resultは、メソッドの最初の行で空の配列を代入しているように、実際にはデータベースから取り出された結果は一切利用をしません。最後には、$resultを返していますが、プログラムを見ると、12のレコードを持つ配列が返されます。配列のひとつの要素は連想配列になっており、JSON記述で示すと {"year": 2010, "month": 1, "startdt": "2010-01-01 00:00:00", "enddt": "2010-02-01 00:00:00"} といった配列になります。つまり、yearとmonthが年と月、そして、日付やタイムスタンプのデータがあれば、startdt以上でenddtより小さいのであれば、yearとmonthで示される年月に含まれるデータであると判断するために利用することができます。

リスト8-2-7 アドバイス定義クラスの内容
use INTERMediator\DB\Extending\AfterRead;

class YearMonthGen implements AfterRead
{
    public function doAfterReadFromDB($result)
    {
        $result = array();
        $year = 2010;
        for ($month = 1; $month < 13; $month++) {
            $startDate = new DateTime("{$year}-{$month}-1 00:00:00");
            $endDate = $startDate->modify("next month");
            $result[] = array(
                "year" => $year,
                "month" => $month,
                "startdt" => "{$year}-{$month}-1 00:00:00",
                "enddt" => $endDate->format("Y-m-d H:i:s"),
            );
        }
        return $result;
    }
}

 この後に紹介するコンテキストは月ごとにデータ集計をすることを意図しており、そのための基本的な検索条件をstartdtとenddtキーの値から生成します。年や月は、もちろん、PHPのプログラムを修正することで、例えば2016年4月から2017年3月といった範囲に変更できます。また、recordsキーの値は1になっていますが、実際に得られたレコードが12個あれば、ページファイルを展開するときに12レコード分の生成を行います。ページネーションにより一定数のレコードずつ表示する場合でない場合は、必要なレコードを含む配列を返すようにPHPのプログラムを作成すれば十分です。ページネーションを伴う場合には、この章の『8-1 サーバーサイドでの処理の追加』にある『アドバイス定義クラスの作成』で説明したAfterRead_WithNavigationインターフェースを実装して、結果の配列だけでなく、レコードの個数や検索条件に合うレコードの総数を返すメソッドも記述してください。

 続いて別のコンテキストであるsummary1を見てみましょう。定義はリスト8-2-8の通りです。こちらはviewキーにあるように、saleslogテーブルにあるレコードを検索します。ページファイルではeverymonthコンテキストを展開したノードの中にあり、relationキーによる定義が検索時に条件として付加されます。つまり、year=2010、month=1のeverymonthコンテキスト内では、summary1コンテキストの内容は「dt >= "2010-01-01 00:00:00" AND dt < "2010-02-01 00:00:00"」という検索条件でsaleslogテーブルを検索した結果、すなわち2010年1月に含まれるレコードに絞られます。しかしながら、extending-classキーにSumForItemsクラスが指定されています。

リスト8-2-8 summary1コンテキストの定義
array(
    'name' => 'summary1',
    'view' => 'saleslog',
    'relation' => array(
        array('foreign-key' => 'dt', 'operator' => '>=', 'join-field' => 'startdt',),
        array('foreign-key' => 'dt', 'operator' => '<', 'join-field' => 'enddt',),
    ),
    'extending-class' => "SumForItems",
),

 SumForItemsクラスのPHPプログラムは、リスト8-2-9の通りです(ファイル全体はこちらから参照できます)。これも、Extending_Interface_AfterReadインターフェースを実装したクラスです。doAfterReadFromDBメソッドの引数$resultには、saleslogテーブルから検索したデータが引き渡されます。このテーブルには1レコードが1件の販売データといった形式ですが、引数で得られるのはその販売データのうち、特定の月のものだけではありますが、1レコードはやはり1件の販売データを示すものです。そして、最初の繰り返し部分で、saleslogテーブルのitemフィールドの値ごとに、totalフィールドの値を合計しています。変数$recordは1レコードの連想配列が設定されるので、$record["フィールド名"] により特定のフィールドの値を取り出せます。arsortにより、合計金額の多い順からソートされます。後半の繰り返しは、上位10件を取り出し、レコードにitemnameおよびtotalpriceキーに対して商品名と合計金額を値として与えています。もちろん、itemnameおよびtotalpriceはページファイル内のフィールド名として利用されます。

リスト8-2-9 SumForItemsクラス
use INTERMediator\DB\Extending\AfterRead;

class SumForItems implements AfterRead
{
    public function doAfterReadFromDB($result)
    {
        $sum = array();
        foreach ($result as $record) {
            if(! isset($sum[$record["item"]]))  {
                $sum[$record["item"]] = $record["total"];
            } else {
                $sum[$record["item"]] += $record["total"];
            }
        }
        arsort($sum);
        $result = array();
        $counter = 10;
        foreach ( $sum as $product => $totalprice )  {
            $result[] = array(
                "itemname"=>$product,
                "totalprice"=>number_format($totalprice)
                );
            $counter--;
            if ( $counter <= 0 )    {
                break;
            }
        }
        return $result;
    }
}

 ページファイルでは、1か月ごとに合計3種類の集計結果を表示しています。残りの2つは、計算方法は違いますが、計算処理の組み込み方法は、summary1コンテキストと同様です。

[利用例] FileMaker Serverで動的にグローバルフィールドを指定する

 FileMakerの特徴として「グローバールフィールド」があります。グローバルフィールドは、どのレコードでもフィールドの値が一定という意味で「グローバル」です。データの実態はデータベースには保存されず、例えばFileMaker Proでログインした場合、そのFileMaker Proで開いた状態、すなわちFileMakerのセッションに対してデータが記録されます。そのため、グローバルフィールドの値は、共有はされません。

 このグローバルフィールドの値を利用して検索条件を与えるようなことがよく行われてきました。例えば、会計システムの場合、複数年度に渡る会計データがデータベースに記録されています。一方、実際に集計したいのは2016年度だけといったことが普通です。この時、年度の指定を、グローバルフィールドで記録すれば、それを基にした検索条件を組み立てたり、あるいは年度を変更するユーザーインターフェースを構築できたりと便利な場合があります。もちろん、グローバルフィールドを使うのがこうした「全体に渡る検索条件」を実現する唯一の実装方法ではありませんが、FileMakerで古くからある方法であり、ユーザーインターフェースを構築するためにはフィールドとしての定義がどうしても必要であることなどから、利用されることは少なくないでしょう。

 グローバルフィールドは、テーブルに定義されるので、通常のフィールドと同じに扱えそうに思われるかもしれませんが、FileMaker ServerのXML共有の仕様では、グローバルフィールドへの値の設定方法と、それ以外のフィールドへの値の設定方法が異なっていることに注意しなければなりません。そのために、コンテキスト定義にglobalキーを用意して、グローバル値の設定ができるようにしてあります。なお、読み出しは通常のフィールドと同様ですが、何もしなければ、グローバルフィールドは空白のままです。XML共有のアクセスは大まかに言って、FileMaker Proで開いて閉じる作業を毎回アクセスのたびに行っているのと同じです。グローバルフィールドに何か値が入った状態で読み出しをしたい場合は、globalキーを使うか、あるいはグローバルフィールドに値を入れるスクリプトを動かすなどの手立てが必要です。通常はグローバルフィールドに値を設定するニーズがほとんどだと思われます。

 コンテキスト定義のglobalキーに値を与えることはできるとはいえ、それをクライアント側で動的に値を変更させるためのJavaScriptの変数等は用意されていません。その場合、クライアント側では、INTERMediator.addConditionメソッドを利用して、まずは普通に検索条件の追加を行います。リスト8-2-10はその例です。なお、INTER-Mediatorに付属するFileMakerのデータベースでは、グローバルフィールドの定義はなされていないので、以下は実際に稼働できるものではなく、作成例としてご覧ください。

リスト8-2-10 コンテキストに動的に検索条件を与えて再合成する
var y = document.getElementById("annual");
INTERMediator.addCondition(
    "product",
    {field: "gYear", operator: "=", value: y}
);
var targetContext = IMLibContextPool.contextFromName("product");
INTERMediator.constructMain(targetContext);

 この例を見ると分かるとおり、productコンテキストが参照するFileMakerデータベースのテーブル内にgYearというグローバルフィールドが必要です。そして、extending-classクラスにリスト8-2-11に示したクラスの名前の「FMGlobalSeparate」を指定したとします。すると、gYearフィールドに対する検索条件は、データベースアクセス時には利用されず、グローバルフィールドの設定のためのパラメーターに置き換えられます。FMGlobalSeparateクラスはほぼ汎用的に作られており、最初の$fieldName変数に代入されている配列の要素に入れたフィールドは、検索条件からグローバルに移動するように作成してあります。

リスト8-2-11 FMGlobalSeparateクラス
class FMGlobalSeparate
    implements INTERMediator\DB\Extending\BeforeRead
{
    public function doBeforeReadFromDB()
    {
        $fieldName = array("gYear");
        $dataSourceName = $this->dbSettings->getDataSourceName();
        $criteria = $this->dbSettings->getExtraCriteria();
        $counter = 0;
        foreach ($criteria as $item) {
            if (array_search($item["field"], $fieldName) !== FALSE) {
               $this->dbSettings->setGlobalInContext(
                    $dataSourceName, "read", $item["field"], $item["value"]);
               $this->dbSettings->unsetExtraCriteria($counter);
            }
            $counter += 1;
        }
    }
}

 まず、全体的に、アドバイス定義クラスなので、$this->dbSettingsというプロパティは、現在のデータベース処理のSettingクラスのインスタンスを参照しています。getDataSourceNameは選択されているコンテキスト名が得られますが、ここでは「product」という名前が得られるはずです。引数なしでgetExtraCriteriaメソッドを呼び出すと、クライアントで動的に指定した検索条件をすべて配列で取り出すことができます。その配列ひとつひとつについてフィールド名を調べ、変数$fieldNameにあるものであれば、そのフィールド名と値をsetGlobalInContextメソッドにより、グローバル変数の設定に追加します。そして、unsetExtraCriteriaメソッドにより、追加の検索条件の配列から削除しておきます。なお、unsetExtraCriteriaメソッドは、配列の要素をunsetするものですので、例えば、要素のインデックスが0、1、2…とある時に、1のインデックスの要素をunsetで削除すると、0、2、3…のように、その他の要素のインデックスは保持された状態になります。INTER-Mediatorはインデックスの数値自体を使わないので、unsetでの削除で問題ありません。

このセクションのまとめ

 INTER-Mediatorの基本的な考え方は、サーバー側でプログラムを作るのではなく、コンテキスト定義とページファイルをもとにして、PHPやJavaScriptのプログラムを作成しなくても、データベース連動のWebアプリケーションの骨格が作成できるようにするという点です。しかしながら、それだけでは利用範囲は限られます。より完成度の高いアプリケーション開発を支援するためのプログラミングインターフェースを用意してあり、その代表的なものがアドバイス定義クラスによる拡張です。

8-3メディア拡張クラスの作成

メディア拡張クラスは、UIから呼び出せる処理プログラムです。アドバイス定義クラスのようにデータベース処理と連携するということなしに、コンテキストに対する処理データを伴って直接呼び出すことができます。この仕組みは、画像などのメディアデータをページに表示するときに、フィールドにあるデータをURLやパスとして解釈する仕組みを汎用的に利用するもので、「メディア拡張クラス」と名付けることにします。

メディア拡張クラスの作成方法

 メディアの中身を取り出す方法については、『メディアファイルの内容の取得』で説明していますので、それを踏まえて説明します。クライアントからの定義ファイルの呼び出しが「def.php」で行える場合、ページファイルのヘッダー部に「<script type="text/javascript" src="def.php"></script>」と記述されていて、定義ファイルへのクライアントからのアクセスが可能になっているとします。そのような状態において、リスト8-3-1のような相対パスのURLにより、「ClassName」で指定したクラス名のクラス(メディア拡張クラス)を生成して、そこに定義されたprocessingメソッドを呼び出します。つまり、mediaキーによるパラメータの値が「class://」をスキームとしたURLになっています。

リスト8-3-1 定義ファイル呼び出しでのメディア拡張クラスの指定
def.php?media=class://ClassName/ContextName/Condition

 この時、呼び出し前に、定義ファイル内に定義されたコンテキスト名「ContextName」で指定したコンテキストに対して、「Condition」で指定した条件を付与した上でクエリーを行い、クエリー結果をprocessingメソッドに引数で渡して利用できるようにします。Conditionは省略できますが、=を含む文字列を検索条件として認識し、最後のひとつだけが適用されます。なお「フィールド=値」の形式で記述しますが、この表記通り、実際の検索でも=演算子で条件を構築します。

 例えば、ページファイル内でのHTMLでの記述例は、リスト8-3-2の通りです。ここでは、aタグによって、「PDF作成」という文字列がリンクとなります。クリックすると、href属性のURLをブラウザが開きます。ここで、ExtendedProcクラスのメソッドを実行することになりますが、その折に、tasklistというコンテキストに対して検索を行います。このコンテキストは、def.phpに定義されている必要があります。ここで、data-im属性があるので、例えば、idフィールド値が24であれば、href属性の最後に24が追加されます。そのURLを開くと、tasklistに対して「id=24」という検索条件が適用されることになります。

リスト8-3-2 ページファイルでの利用例
<a href="def.php?media=class://ExtendProc/tasklist/id="
   data-im="tasklist@id@#href">PDF作成</a>

 拡張プログラムを記述するクラスExtendProcについては、リスト8-3-3のように定義します。また、ファイルのロードを自動的に行うために、ファイル名は「ExtendProc.php」としておいて、定義ファイルと同じ階層に配置します。クラス名は、mediaキーの値に記述するので、その都度異なる名前を付けても構いません。一方、クラスの定義内容は、processingメソッドがあり引数が2つなのは、常に同じです。processingメソッドは返り値は必要ありません。$contextDataには、mediaキーに指定したコンテキストと検索条件によって検索された結果が得られます。1レコードを示すフィールド名をキーとした連想配列がレコードの数だけ含む配列として得られます。2つ目の引数には、定義ファイルに記述したIM_Entryの第2引数の値がそのまま得られます。

リスト8-3-3 新たに定義するクラスの基本構成
class ExtendProc
{
    public function processing($contextData, $options) {
    }
}

 このExtendProcクラスは、aタグのリンク先に含まれています。processingメソッドの記述は、ページファイルでの用途に応じた記述を行います。例えば、実際にPDFを生成したいのなら、processingメソッド内でデータベースから取り出した結果をもとにしてライブラリ等を使ってPDFを生成します。そして、header関数を使って適切なMIMEタイプを返し、さらにechoステートメント等でPDFデータを出力します。PDFの生成のサンプルは、INTER-MediatorのフォルダーのSamples/Sample_pdfにあります。

 もし、リンク先からHTMLやテキストを返したいなら、echoステートメント等で、文字列を返します。例えば、リンクをクリックすればiCalendar対応のデータがダウンロードされるようにしたい場合には、header関数とechoステートメントで、適切なMIMEタイプのレスポンスヘッダーを返して、iCalendar形式のテキストをechoで出力します。

 他には、データに応じて加工した画像を返したり、圧縮ファイルを返すなどの使い方もあります。これも、基本的に同様で、processingの中でサーバーからの応答を記述することになります。なお、processingの最初の引数で、必要なデータを得ておくのが手軽な方法ですが、それで足りない場合には、さらにデータベースアクセスを行うプログラムを記述する必要があります。

[利用例] テーブルの内容をエクスポート

 ボタンやリンクをクリックすると、「定義ファイル名.php?media=....」のリンクに移動するように、例えば、location.hrefへの代入を行うようなJavaScriptを書いておきます。リンクやクリックによって、特定のテーブルのエクスポートが可能なメディア拡張クラスの定義が可能です。単独のテーブルはもちろんですが、自分のニーズに合ったエクスポート結果が欲しいなら、そのような結果が得られるビューを用意すればいいので、汎用的な機能といえます。そのようなクラスを作成しましょうということに以前はなっていたのですが、汎用的なエクスポートのクラスはINTER-Mediatorの内部ですでに用意されているので、そのクラスを使用すれば簡単にエクスポートの機能が実装できます。例えば、定義ファイルdef.phpに、コンテキストalldataが定義されているとします。その場合、リスト8-3-4ようなURLへのリンクへジャンプすれば、alldataコンテキストの内容がCSVファイルとしてダウンロードされるはずです。INTERMediator\DB\ExportがINTER-Mediatorで用意されたエクスポートのための汎用クラスです。

リスト8-3-4 新たに定義するクラスの基本構成
def.php?media=class://INTERMediator\DB\Export/alldata

 INTERMediator\DB\Exportの動作については、以下のようになっています。

 INTERMediator\DB\Exportの動作をカスタマイズすることが可能です。そのためには、このクラスを継承したメディア拡張クラスを作って、media=class://の後には定義したクラスを指定します。以下は、カスタマイズ可能なプロパティやメソッドをまとめたものですが、もちろん、変更したいプロパティのみのクラスで構いません。また、keysAndLabelsプロパティ以外は、そのプロパティの規定値を示しています。

リスト8-3-5 エキスポートをカスタマイズするメディア拡張クラス
class ExportSample extends INTERMediator\DB\Export
{
    protected $keysAndLabels = [
           "unitprice" => "単価","name" => "商品名","taxrate" => "消費税率",
    ];
    protected $encoding = "UTF-8";
    protected $fileNamePrefix = "Exported-";
    protected $fileExtension = "csv";
    protected $fieldSeparator = ',';
    protected $quote = '"';
    protected $endOfLine = "\n";

    public function processing($contextData, $options) {}
}

 keysAndLabelsは、既定値は[]となっており、その場合は、コンテキストのフィールド名がそのままラベル行として出力されます。このプロパティに「フィールド名=>ラベル名」の連想配列を指定すると、エクスポートしたファイルの1行目はラベル名が記述できます。また、ここで記載したフィールドのみが出力されます。

 encodingプロパティは、エクスポートされるデータのエンコーディングで、PHPで利用できるエンコーディング名を指定します。Excelで読み込みがすぐできるようにするには、ここに「SJIS」等を設定してください。ダウンロードされるファイル名の既定値は、"{$fileNamePrefix}{日付時刻}.{$fileExtension)"で決定されます。通常は、このような形式でおおむね済むと思われます。

 ファイル内部の形式としては、フィールドの区切りをfieldSeparatorプロパティ、文字列の囲みをquoteプロパティ、改行コードをendOfLineプロパティで指定することができます。タブ区切りなどの違い形式にした場合には、これらのプロパティを変更することになります。

 継承したクラスではprocessingメソッドを指定することができます。このメソッドを指定すれば、元のクラスとは全く異なる出力形式のエクスポートも可能です。例えば、最初の方に数行の特別な行を入れたいと言ったことや、JSONで出力したいということであればprocessingメソッドを自分で記述します。しかしながら、そうなったら、むしろExportクラスを継承しない方がもはや早いかもしれません。自分でprocessingメソッドを記述する場合は、Exportクラスのprocessingを「parent::processing($contextData, $option);」などで呼び出して、その処理との違いを自分で記述するような場合に限定されると思われます。なお、CSV化はleague/csvというライブラリを利用しており、INTER-Mediatorではこのライブラリを必ずインストールするようにしているので、このライブラリの利用も検討してください。

[利用例] メディアアクセスクラスを利用してPDFを生成する

 サンプルプログラムには、データベースにあるデータをもとにPDFを生成可能なものがあります。PDFを生成するためのライブラリとしてtcpdfを利用しており、INTER-Mediatorのインストール時に自動的に入るようになっています。実際にサンプルを稼働させてみましょう。演習環境を利用している場合には、ブラウザーで「http://localhost:9080」に接続し、そこにある「サンプルプログラム」のリンクをクリックして、サンプルプログラムの一覧を表示します。そして、「PDF Generating」の行の「show」をクリックすると、図8-3-1のように、まず、商品一覧のようなページが見えています。そして、PDFのリンクをクリックすると、図8-3-2のように対応するレコードの内容がPDFに変換され、ブラウザーの画面に表示されます。PDFの見え方は、ブラウザーの設定により異なる可能性もあります。

図8-3-1 商品の一覧ページにPDFリンクがある
図8-3-2 特定のレコードのデータがPDFに表示された

 実際にどのようにページが構築されているかを確認しましょう。まず、定義ファイルは「contexts_MySQL.php」、ページファイルは「list_detail_MySQL.html」です。ページ上に商品の一覧が出ていますが、この商品名などのコンテキストは、puroductlistコンテキストを利用しています。定義ファイルのコンテキストの定義はリスト8-3-6の通りで、viewキーの値がproductであり、productテーブルの内容を一覧しています。queryキーによる検索条件は、nameフィールドに何か入力されているという意味ではありますが、あまり深い意味はありません。ともかく、productテーブルのレコードをページに一覧表示しています。

リスト8-3-6 productlistコンテキストの定義
array(
    'records' => 10,
    'name' => 'productlist',
    'view' => 'product',
    'key' => 'id',
    'query' => array(array('field' => 'name', 'value' => '%', 'operator' => 'LIKE')),
    'sort' => array(array('field' => 'name', 'direction' => 'ASC'),),
),

 ここでページファイルの中でもPDFというリンク部分のタグがどのようになっているかを見てみましょう。そこを抜き出したのが、リスト8-3-7です。aタグにより、PDFという文字列を囲んでいます。そして、data-im属性を見ると、productlistコンテキストのidフィールドの値を、aタグ要素のhref属性内の$の文字と置き換えるという指定になっています。idフィールドはもちろん、主キーとなる連番のフィールドです。このdata-im関数により、もしidフィールドの値が「4」ならば、aタグ要素のhref属性は「contexts_MySQL.php?media=class://PDFSample/productlist/id=2/」といったURLになります。つまり、定義ファイルへのクライアントからのアクセスがあり、mediaというキーによるパラメーターが付与されているとうことです。mediaキーの値は「class://PDFSample/productlist/id=2/」です。

リスト8-3-7 PDFリンクのタグ要素
<a href="contexts_MySQL.php?media=class://PDFSample/productlist/id=$/"
                   data-im="productlist@id@$href">PDF</a>

 このmediaキーの値「class://PDFSample/productlist/id=2/」は次のように解釈されます。まず、class:なので、引き続いて、クラス名、コンテキスト名、検索条件が記述されることになります。ここではまず2つ目の項目である「productlist」があることに注目します。これにより、PDFのリンクをクリックして定義ファイルへアクセスしたとき、INTER-Mediatorはまずproductlistコンテキストに対して検索を行います。この時の条件「id=2」は、idフィールドが2であるレコードを意味します。idフィールドは主キーなので、ひとつのレコードが検索されます。そして、その検索した結果のレコードを引数に伴って、リスト8-3-8に示すPDFSampleクラス(ファイルはこちらから参照できます)のprocessingメソッドを呼び出します。ここで、idが2のレコードは、nameフィールドが「Orange」、unitpriceフィールドが「1540」などになっているので、processingメソッドの最初の引数には、JSON形式で記述すると、[{"id": 2, "name": "Orange", "unitprice": 1540, ...}] といった連想配列の配列が得られます。レコードがひとつしかない場合でも、全体は配列になります。例えば、nameフィールドの値は「$contextData[0]['name']」で得られます。processingメソッドの最初の部分で検索して得られたデータを変数に入れ、あとはtcpdfの機能を使ってPDFを生成しています。PDFの生成に関する部分は省略しますが、最後のOutputメソッドにより、PDFのデータが出力されます。ここでは、aタグ要素であったことを思い出してください。つまり、リンクをクリックすることにより、定義ファイルが呼び出されて、PDFのデータを出力します。したがって、header関数を使って応答のヘッダーのContent-Typeキーの値などを適切に設定しておくことで、ページ上に表示したり、あるいはダウンロードしたりといったことを、ブラウザーの設定に依存する面はあるかもしれませんが、ある程度はコントロールできるでしょう。

リスト8-3-8 PDFSampleクラス
class PDFSample
{
    function processing($contextDatas)
    {
        $prodId = $contextData[0]['id'];
        $prodName = $contextData[0]['name'];
        $unitPrice = $contextData[0]['unitprice'];
        $pFile = $contextData[0]['photofile'];
        $timestamp = new DateTime();
        $tsString = $timestamp->format("Y-m-d H:i:s");
        $fileName = "{$prodId}.pdf";
        require_once './tcpdf/tcpdf.php';

        $pdf = new TCPDF("P", "mm", "A4", true, "UTF-8");
        $pdf->setPrintHeader(false);
        $pdf->setPrintFooter(false);
        $pdf->SetMargins(0, 0, 0, 0);
        $pdf->AddPage();
        $pdf->setTextColor(100, 100, 100);
        $pdf->SetFont('', '', 14);
        $pdf->Text(40, 40, "Product ID: {$prodId}");
        $pdf->Text(40, 50, "Product Name: {$prodName}");
        $pdf->Text(40, 60, "Unit Price: {$unitPrice}");
        $pdf->Text(40, 70, "Today: {$tsString}");
        $pdf->Image("../Sample_products/images/{$pFile}", 40, 80, 100);

        header("Content-Type: application/pdf");
        header("Content-Disposition: attachment; filename=\"{$fileName}\"");
        header('X-Frame-Options: SAMEORIGIN');
        $pdf->Output();
    }
}

このセクションのまとめ

 メディア拡張クラスは、画像等のメディアを参照するための仕組みをさらに汎用的に利用できるようにしたものです。コンテキストとの絡みなどが若干ややこしくなりますが、データベースの処理を自動化し、追加の処理を自身で記述できるため、拡張機能のひとつとして利用できます。とは言え、この仕組みを利用して一番よく作っている機能はエクスポートです。INTER-Mediatorで汎用的なエクスポートクラスを用意したので、クラス自体を定義する機会はさらに減っています。

8-4独立したスクリプトでINTER-Mediatorを利用する

cron等で一定時間毎にバッチ処理を行ったり、あるいはWebサーバの公開ドキュメントの中にある.phpファイル、つまり独立したWebページの中からINTER-Mediatorを使いたいという場合もあるでしょう。その場合のポイントを説明します。

プログラム作成に必要なこと

 この種のプログラムを作ることで重要なことは、INTER-Mediatorの読み込みです。定義ファイルの最初にrequire_onceで「INTER-Mediator.php」を読み込みますが、それと同じことをどこかで行う必要があります。スクリプトの最初でもいいのですが、データベース処理などはクラスとして定義すれば、Proxy_ExtSupportトレイトを組み込んで、データベース処理が手軽にできるということもあるため、単なるフラットなスクリプトではなく、主要処理部分はクラスとして定義するのが適切です。そうであれば、そのクラス定義の最初に、INTER-Mediator.phpを取り込むステートメントを記述しておくのが一般的でしょう。そのクラスの中では、例えば、dbReadメソッドでデータベースの読み出し等ができるようになります。

 cron等での一定時間毎に自動的に起動されるスクリプトでも原則は同じですが、phpスクリプトを直接稼働させるのではなく、通常のシェルスクリプトを記述して、その中ではスクリプトファイルのあるディレクトリをカレントにし「php スクリプトファイル.php」といったphpのコマンドラインインターフェースを利用して稼働するなどの工夫が必要になるかもしれません。

[利用例] Web APIを作成する

 サンプルプログラムには、Web APIとして稼働するものがあります。実際にサンプルを稼働させてみましょう。演習環境を利用している場合には、ブラウザーで「http://localhost:9080」に接続し、そこにある「サンプルプログラム」のリンクをクリックして、サンプルプログラムの一覧を表示します。そして、「API」の行の「show」をクリックすると、図8-4-1のようなフォームが見えます。このサンプルは、データベースに用意されたproductテーブルへidフィールド値を指定して検索を行い、その結果を上のテキストエリアに表示します。下のテキストエリアは、デバッグ情報を表示します。idは初期値では1〜5が用意されていますが、それ以外の値を指定するエラーメッセージが返されます。Web APIのデータベースアクセス部分はINTER-Mediatorで作成したものですが、HTMLページの方はごく簡単なWeb APIのデモ実行環境です。

図8-4-1 Web APIのサンプルの実行結果

 実際にどのようにページが構築されているかを確認しましょう。まず、ページのHTMLファイルは「api-test.html」です。このファイルのヘッダー部分にはSCRIPTタグによって定義ファイルへのアクセスを行う記述はありません。つまり、単独で稼働しているHTMLファイルです。id属性値が設定されたテキスト入力要素は、検索条件に含めるidフィールドの値を設定するテキストフィールド(id属性値は「product_id」)と、結果を表示するテキストエリア(id属性値は「result」)、デバッグ情報を表示するテキストエリア(id属性値は「log」)です。ボタンを押して呼び出されるプログラムは、リスト8-4-1に示しました。一部、重要でない部分は省略しています。

リスト8-4-1 HTMLファイルにあるAPIを呼び出すプログラム
function callAPI() {
    var myRequest, jsonObject;
    document.getElementById("result").value = "";
    document.getElementById("log").value = "";
    var id = document.getElementById("product_id").value;
    try {
        myRequest = new XMLHttpRequest();
        myRequest.open("GET", "api.php?id=" + id, true);
        myRequest.onreadystatechange = function () {
            switch (myRequest.readyState) {
				:
                case 4:
                    try {
                        jsonObject = JSON.parse(myRequest.responseText);
                    } catch (e) {
                        jsonObject = null
                    }
                    if (jsonObject.data) {
                        document.getElementById("result").value = JSON.stringify(jsonObject.data);
                        document.getElementById("log").value = JSON.stringify(jsonObject.log);
                    } else {
                        document.getElementById("log").value = myRequest.responseText;
                    }
                    break;
            }
        };
        myRequest.send();
    } catch (e) {
        document.getElementById("result").value = "Exception in commnication."
    }
}

 最初に2つのテキストエリアのクリア、そして検索条件をid変数に設定し、tryで囲まれた部分に入ります。ここは、通常のAJAX通信を行っていますが、通信先は、同じフォルダーにあるapi.phpというファイルで、URLのパラメーターにidという名前のキーで、idフィールドの値を指定しています。例えば、idフィールドの値が3であれば、「api.php?id=3」というURLが得られて通信を行うことになります。通信後、onreadystatechangeプロパティに設定した関数が呼び出され、通信が成功すればreadyStateプロパティの値が4になります。その場合、通信結果のJSONデータをパースしたのち、dataプロパティ、logプロパティをそれぞれテキストエリアに表示させています。api-test.htmlのプログラムはこのように簡単なAJAX通信を行うものです。

 Sample_APIフォルダーにあるもうひとつのファイル「api.php」が、Web APIの本体で、このひとつのファイルで構成しています。ファイルのコメント以外をリスト8-4-2に示します。単に$_GETグローバルから、idに対する値を得て、実質的にはDBOperationクラスに任せています。このapi.phpとDBOperation.phpの2つのファイルは同じフォルダにあります。api.phpはクライアントから呼び出される窓口業務的なことだけにしており、ここではINTER-Mediatorの機能は使っていません。

リスト8-4-2 Web APIのサンプル
<?php

require_once(dirname(__FILE__) . '/DBOperation.php');

$pid = mb_eregi_replace("/[^0-9]/", "", $_GET["id"]);
if ($pid < 1) {
    echo json_encode(array("ERROR" => "Invalid Product Number."));
    exit();
}

$result = (new DBOperation())->readData($pid);
echo json_encode($result);

 DBOperationクラスのプログラムは、リスト8-4-3に示します。ファイルは「DBOperation.php」です。最初にINTER-Mediator.phpを読み込んでいるところがポイントで、その結果、INTER-Mediatorの諸機能が使えるようになります。データベース処理を手軽に記述したいので、Proxy_ExtSupportトレイトを組み込んでいます。これにより、dbRead等でデータベース処理ができますが(『PHPでの拡張クラス内でのデータベース処理』を参照)、コンテキストの定義はプロパティとして定義して、複数のメソッドで使っています。なお、api.phpから呼び出されるのはreadDataメソッドのみです。

リスト8-4-3 DBOperationクラス
<?php

require_once(dirname(__FILE__) . '/../../INTER-Mediator.php');

use INTERMediator\DB\Proxy_ExtSupport;

class DBOperation
{
    use Proxy_ExtSupport;

    private $contextDef = [
        [
            'name' => 'product',
            'key' => 'id',
//            'query' => array(array('field' => 'name', 'value' => '%', 'operator' => 'LIKE')),
//            'sort' => array(array('field' => 'name', 'direction' => 'ASC'),),
        ],
    ];

    public function readData(int $pid): array
    {
        $this->dbInit($this->contextDef, null, null, 2);
        $condition = ["id" => $pid];
        $pInfo = $this->dbRead("product", $condition);
        return ["data" => $pInfo, "log" => $this->getAllLogs()];
    }

    private function getAllLogs(): array
    {
        $logInfo = [];
        $logger = Logger::getInstance();
        $logInfo['error'] = $logger->getErrorMessages();
        $logInfo['warning'] = $logger->getWarningMessages();
        $logInfo['debug'] = $logger->getDebugMessages();
        return $logInfo;
    }

    public function createData(string $prodName, int $prodPrice)
    {
        $this->dbInit($this->contextDef, null, null, 2);
        $data = ["name" => $prodName, "unitprice" => $prodPrice];
        $pInfo = $this->dbCreate("product", $data);
        return ["data" => $pInfo, "log" => $this->getAllLogs()];
    }

    public function updateData(int $pid, string $prodName, int $prodPrice): array
    {
        $this->dbInit($this->contextDef, null, null, 2);
        $condition = ["id" => $pid];
        $data = ["name" => $prodName, "unitprice" => $prodPrice];
        $pInfo = $this->dbUpdate("product", $condition, $data);
        return ["data" => $pInfo, "log" => $this->getAllLogs()];
    }
}

 readDataメソッドを詳細に見てみましょう。ここでは、処理ログも詳細に取りたいので、デバッグモードを「2」で実行したいところです。しかしながら、dbRead等は、通常はデバッグモードをfalseで実行します。そのため、dbInitメソッドを呼び出して、デバッグモードを2にします。このとき、コンテキスト定義を行なって、このコンテキスト定義を先のdbReadで利用します。ログについてはgetAllLogsメソッドにおいて、Loggerクラスのインスタンスを求めて、3種類のメッセージをそれぞれ取り出しています。

 同様に、レコード作成やレコード更新のメソッドも作ってありますが、現状ではapi.phpからは呼び出されていません。作成のサンプル程度で見ておいてください。実際にはプログラムがもっと長くなるでしょうし、色々なテーブルやビューなどの取得メソッドを個別に記述するなど、クラスの定義自体も長くなると思われます。

このセクションのまとめ

 INTER-Mediator外部のプログラムでも、INTER-Mediatorを取り込むなどすれば、データベース処理などが手軽に利用できます。INTER-Mediatorでユーザインターフェースを作る以外の部分でも、INTER-Mediatorを活用できます。

8-5ブラウザーを判断するページ

ブラウザーの判定を行い、サポートするブラウザーとしないブラウザーの表示を切り替える方法を紹介します。特定のバージョン以前のブラウザを利用しないようにすることは設定だけで可能ですが、現在の開発状況では、むしろ、Internet Explorerを排除するのが最優先となるかと思われます。その点やあるいはWebアプリケーションの動作の点で、プログラミングを行う方がより良い結果となりますので、この章で説明をします。

ブラウザー制限のための戦略

 INTER-Mediatorでブラウザーの制限をする理由は、JavaScriptの対応がブラウザーによってまちまちであることが理由です。INTER-Mediatorのフレームワクーク自体は、HTML5に対応したブラウザーを使用することを前提にしているので、当然ながら、「対応しないブラウザー」が存在します。このように、フレームワークが動作対象としていないブラウザーの排除のために仕組みがまずは必要です。さらに、アプリケーションによっては、サポートあるいはポリシーの関係で、特定のブラウザーだけに限定したい場合もあるかもしれません。その場合、さらなる制約をかけたいと考えるでしょう。INTER-Mediatorでは、ブラウザーとそのバージョンに対しての制約を設定することができます。

 一方、「ブラウザーでJavaScriptが稼働する」ということも成立していない可能性も考えます。利用者が、JavaScriptの実行ができないように、ブラウザーを設定している可能性があります。その場合、当然ながら、INTER-Mediatorは稼働しませんので、なんらかのメッセージを出したいところです。この場合の対策を実現するために、HTMLページの内容は、JavaScriptが稼働していない、あるいは稼働しようとしても動かないくらい古いブラウザーに対するメッセージだけを表示して、実際のページのコンテンツは初期状態では非表示にしておきます。そして、JavaScriptが切りあるいはDOM非対応なら、そのまま何もしないようにします。そのための判定をJavaScriptで行いますが、そのプログラム自体が実行されなくても、HTMLページの初期状態がそのまま見えるだけです。つまり、JavaScriptが動かない場合には、そのためのメッセージを表示して、それ以上は何もしません。

 もし、ブラウザーが対応しているバージョンであれば、非対応の場合のメッセージを見えないようにし、それまで非表示だったページ内容を表示するようにして、ページ生成の処理を行います。つまり、非対応メッセージは自動的に消されるという動作を期待しますが、これについては1行で済む処理なので、プログラムを記述していただく必要があります。具体的にはあとの演習を参照してください。

JavaScriptの稼働ができない場合の対処

 JavaScriptの稼働ができないような場合に何らかのメッセージを出す仕組みとしては、まず、ページファイルのbodyタグ内での対処があります。bodyタグ内ではアプリケーションの表示を行う部分をdivタグ等でまとめて初期状態では、スタイルとして、display: none を指定しておきます。そして、そのdivタグとは別に、JavaScript稼働ができない場合のメッセージを表示します。それらメッセージは自由に作成できますが、JavaScriptが稼働できた場合には消したいので、やはりそれらをひとつのdivタグにまとめるということが典型的な手法になります。例えば、ページファイルをリスト8-5-1のように作成します。

リスト8-5-1 JavaScript非稼働ブラウザ等を考慮したメッセージの追加
<!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="def16.php"></script>
      <script type="text/javascript">
        INTERMediatorOnPage.doAfterConstruct = function () {
            var node = document.getElementById('container');
            node.style.display = "block"
        }
    </script>
</head>
<body>
<div id="nonsupportmessage" style="background-color:#333333">
    <div style="text-align:center;color:yellow">
      If you see this, you must use any supported
      web browser with Javascript enabled.
    </div>
    <div style="text-align:center;color:yellow">
      この表示はサポート対象外のWebブラウザーを使っているために表示されています。
      対応ブラウザーをJavaScriptを有効にした上でお使い下さい。
  </div>
</div>
<div id="container" style="display: none">
  <table>
    <tbody>
      <tr>
        <td data-im="postalcode@f3"></td>
        <td data-im="postalcode@f7"></td>
        <td data-im="postalcode@f8"></td>
        <td data-im="postalcode@f0"></td>
      </tr>
    </tbody>
  </table>
</div>
</body>
</html>

 ページファイルのボディ部には、id属性が「nonsupportmessage」のDIVタグ要素があり、そこにはJavaScriptがオフになっていたり、極端に古いブラウザーを使っていた場合を想定したエラーメッセージが記述されています。一方、実際に見せたいページの内容は、id属性が「container」のDIVタグ要素で囲って記述しておき、こちらも初期状態は非表示にしておきます。このように、ページの中の最上位のノードにおいて、「JavaScriptが動かなかった時に表示される内容」と「動いた時に表示される正しい内容」の2つの要素を求めておきます。そして、JavaScriptが動かない場合はそのままid属性が「nonsupportmessage」のDIVタグ要素だけが見えていますが、INTER-Mediatorが稼働して正しくページ合成すると逆にid属性が「nonsupportmessage」のDIVタグ要素は非表示に、id属性が「container」のDIVタグ要素は表示にして、ページの内容が見えるとともにエラーメッセージは見えないようにします。

 ページをロードしたときにページ合成が自動的に行われますが、id属性が「nonsupportmessage」のDIVタグ要素がある場合には、オプション設定のbrowser-compatibilityあるいはparams.phpの$browserCompatibility変数の設定を使用して、対応ブラウザーかどうかのチェックを行います(詳細は事項の『ブラウザーを限定するための指定』を参照)。対応ブラウザーでない場合には、何もせず、id属性が「nonsupportmessage」のDIVタグ要素だけが見えている状態になります。対応ブラウザーであれば、ページ合成を行います。なお、このままだと、id属性が「container」のDIVタグ要素は非表示のままです。そこで、ページ合成が終わったときに呼び出されるメソッド、INTERMediatorOnPage.doAfterConstruct((『8-5 ブラウザーを判断するページ』を参照))において、ページ要素を含むid属性が「container」のDIVタグ要素のdisplayスタイルシート属性をblockに変更するJavaScriptのプログラムを実行して、ページ要素を見えるようにしています。つまり、ブラウザー判定に合致して対応ブラウザーであると判定された場合、ページ合成を行い、ページ要素を見えるようにします。

 なお、ブラウザーの判定を含むページファイルの雛形として、INTER-MediatorフォルダーのSamples/templatesにあるpage_file_complex.htmlというファイルがあります。ページファイルを作るときには、このファイルをコピーして作成してもいいでしょう。もちろん、定義ファイルの参照パス等は適時変更が必要です。

ブラウザーを限定するための指定

 ブラウザーに関連する設定は、定義ファイルのIM_Entry関数の2つ目の引数(定義ファイルエディターではOptionsの領域)に、browser-compatibilityキーで記述します。このキーに指定する値は少し複雑で、表8-5-1のようになっています。指定例はリスト8-5-2に記載します。ブラウザーの種類はJavaScriptの標準APIに含まれるnavigator.userAgentを利用しており、大文字小文字は関係なく指定をします。また、params.phpファイルにおいても、$browserCompatibility変数で同様な指定が可能です。こちらは、同様な形式の配列をPHPの形式で指定します。定義ファイルよりも、params.phpファイルの方が優先的に採用されます。なお、このブラウザを限定する機能は、ページファイル内にid属性が「nonsupportmessage」あるいは、params.phpファイルの$nonSupportMessageId変数で指定した文字列のものが存在する場合にだけ機能します。

次元指定内容指定する値の例
第1次元ブラウザーの種類chrome, msie, firefox, opera, safari, trident, webkit
第2次元配列(OSを示すキー)mac, win
バージョン記述文字列3+, 4-
第3次元バージョン記述文字列(第2次元が配列のとき)3+, 4-
表8-5-1 対応ブラウザーを指定する配列の構成
リスト8-5-2 対応ブラウザーを指定する配列の定義ファイルでの記述例
IM_Entry(
  array( ... ),
  array(
    'browser-compatibility' => array(
      'chrome' => '1+',
      'firefox' => '2+',
      'msie' => '8+',
      'opera' => '1+',
      'safari' => array(
        'win' => '4+', 
        'mac' => '3+',
      ),
      'trident' => '5+',
      'webkit' => '1+',
   ), ....
  ),
	....
);

 Internet Explorerは「msie」で選択ができるのは、Ver.10までで、それ以降はtridentというキーワードで判定します。しかしながら、Internet Explorerの最新版でもJavaScript対応バージョンが古く、INTER-Mediatorが稼働しないので、Internet Explorerの判定と排除は別の方法(この後の『Internet Explorerの排除』)を利用します。OSの種類はnavigator.platform、バージョンはnavigator.appVersionやnavigator.userAgentに含まれる数値から適宜判断して取得しています。OSの種類を指定しないと、OSの種類に関係なく判定されます。バージョン番号のあとに+をつければ、その番号を含むより新しいブラウザーも含まれます。なお、navigator.userAgentDataについてはVer.12で対応予定です。また、最近のスマートフォン用ネイティブアプリケーションでは、内部にWebブラウザーの機能を持つこともあります。そのようなブラウザー内での動作をさせるには、ブラウザーの種類に「webkit」という文字列を指定します。

 なお、ここで、browser-compatibilityキーに対する連想配列のキーにおいては、safariよりも前にchromeを指定してください。Chromeのnavigator.userAgentにはSafariという単語が含まれているためです。browser-compatibilityキーがない場合には、判定において、すべてのブラウザーは非対応とみなします。このキーに、対応するブラウザーのバージョンを記述するというのが基本です。

 この設定による判定によって、対象外と判断されたブラウザの場合は、図8-5-1のような表示を行います。

図8-5-1 browser-compatibilityの定義外のブラウザで表示した場合
リスト8-5-3 対応ブラウザーを指定する配列の定義ファイルでの記述例
IM_Entry(
  array( ... ),
  array(
    'browser-compatibility' => array(
      'chrome' => '1+',
      'firefox' => '2+',
      'msie' => '8+',
      'opera' => '1+',
      'safari' => array(
        'win' => '4+', 
        'mac' => '3+',
      ),
      'trident' => '5+',
      'webkit' => '1+',
   ), ....
  ),
	....
);

 ここでのメッセージは、以前のInternet Explorerを考慮したものであったりするので、別のメッセージに変えたいと思うかもしれません。その場合、params.phpファイルでメッセージのカスタマイズを行います。例えば、リスト8-5-4のような変数定義をparams.phpファイルで行います。

リスト8-5-4 システムメッセージを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あるいは他の一般的なブラウザをご利用ください。";

 $messages変数で2次元の配列を記述し、1次元目が言語、2次元目がメッセージ番号で、ブラウザの言語によって定義したメッセージを表示します。ブラウザ判定のエラーメッセージは、1022番の要素として定義されているので、番号はこのまま指定します。

Internet Explorerの排除

 INTER-Mediator Ver.5までの仕組みでは、一部問題はあるもののINTER-Mediator自体はInternet Explorerで稼働しました。しかしながら、Ver.6で非同期通信をサポートした段階で、Internet Explorer Ver.11でも稼働しない状況になりました。この時、ブラウザの判定すらできない、つまりINTER-Mediatorのロードすらできない状態になりました。ここでどのような対処をするかを検討したのですが、その時点ですでにInternet Explorerのサポート終了も近づいたため、「Internet Explorer自体では稼働しない」という状況が基本的な対処で問題ないと考えました。実際、その時期では、対象ブラウザにInternet Explorerを含めない案件の方が増えてきたからです。しかしながら、INTER-Mediator自体の動作もできないのでは「Internet Explorerはサポート外なのでを使わないで欲しい」という意図すら伝わりません。そこで、そうした対処が必要な場合は、定義ファイルに仕込みを行います。例えば、定義ファイルの冒頭にリスト8-5-5のようなプログラムを記述します。

リスト8-5-5 Internet Explorerを排除するための定義ファイル
<?php

$userAgent = $_SERVER['HTTP_USER_AGENT'];
if (stripos($userAgent, 'MSIE') !== false || stripos($userAgent, 'Trident') !== false) {
    echo "location.href='/ie.html';";
    exit;
}

require_once('lib/INTER-Mediator/INTER-Mediator.php');

IM_Entry(...);

 上記のようなプログラムがあれば、Internet Explorerを使っていると、定義ファイルへの応答は「location.href='/ie.html';」というJavaScriptの1行プログラムになります。定義ファイルは、ページファイルのscriptタグで取り込まれるのですが、その結果、この1行プログラムが実行されて、ブラウザでは/ie.htmlというファイルにリダイレクトされます。もちろん、ie.htmlファイルを作っておいて、そこに適切なメッセージを各種言語で記載しておくという対処が基本になります。この方法は、古いJavaScriptでもいいので、location.hrefへの代入でリダイレクトさえできれば、コントロールが可能です。

 複数の定義ファイルに同じプログラムを記述するのが抵抗があるのであれば、require_onceより前の部分を単独のphpファイルで記述して、そのファイルをrequire_once等で読み込んでもいいでしょう。

 この方法を利用すると、一定の日時まで、あるいは一定の日時以降は、別のページにリダイレクトさせて、ページを利用できる期間を設定するということにも使えます。また、IPアドレスに対する応答を切り替えるということにも利用できます。

このセクションのまとめ

 非対応のブラウザーでアクセスしたときにメッセージを表示して、ページ合成を行わない仕組みをINTER-Mediatorは持っています。その仕組みを利用するためのページファイルの作成方法などがあり、どこまで制限を行うかによって対処を検討する必要があります。

8-6サービスサーバの役割と稼働

INTER-Mediatorは、サーバサイドではPHPでのプログラムが稼働しますが、常駐したサービスを提供するために「サービスサーバ」というNode.jsによるサーバを起動することもできます。このセクションでは、サービスサーバの動作原理と管理手法を説明します。

サービスサーバーの役割

 サービスサーバは、サーバ上で常駐するサーバで、マルチクライアントでの同期(『5-3 マルチクライアントでの同期』で解説)を実現するために稼働するものです。現状はもうひとつ役割があって、それは、入力結果の検証を行うバリデーション(『3-5 バリデーション』で解説)の設定があるとき、クライアントサイドだけでバリデーションを行うのではなく、サーバに伝達されたデータについても同一の式を検証して適用します。クライアント同期はサービスサーバが稼働していないと同期処理は動きません。サーバサイドのバリデーションはサービスサーバが起動すれば稼働しますが起動していない場合にはその処理は行われないようになっています。

 動作上は、PHPからサービスサーバのHTTPのリクエストを出してさまざまな処理を行いますので、原理的にはINTER-Mediatorのサーバとは別にサービスサーバを稼働するということもできます。もちろん、単一のサーバで運用することもできます。少しややこしいですが、PHPで実装しているINTER-Mediatorのメインサーバと、サービスサーバ、そしてクライアントの関係を図8-6-1に示しました。1から順番に追っていただくと、ページの表示とその後の更新の伝達において、それぞれのモジュールがどんな役割を持っているのかがわかると思います。

図8-6-1 クライアント同期とサービスサーバ

サービスサーバーを起動する方法

 まず、サービスサーバのインストールですが、INTER-Mediatorではcomposerでnode.jsをインストールして、それを使って自動起動を試みます。そのため、何もしなくても動き始めるように見えるかもしれませんが、背後では色々と処理は進めています。なお、既定の状態では、サービスサーバは起動しないので、サービスサーバを手動で動かすことになります。サービスサーバのパラメータは、params.phpファイルに記述して、PHP側でそれを読み取って引数に指定してnodeを起動しているので、その必要なパラメータが分かれば、手動でのサービスサーバの起動は可能です。

 

変数既定値動作
$notUseServiceServertruefalseにするとサービスサーバを起動する
$activateClientServicefalsetrueにするとクライアント間の同期が機能する
$serviceServerProtocolwsクライアント間同期で、クライアントがサービスサーバに接続するプロトコル。指定可能なものはws、http、wss、https
$serviceServerHostlocalhostクライアント間同期で、クライアントがサービスサーバに接続するホスト名
$serviceServerPort11478クライアント間同期で、クライアントがサービスサーバに接続するポート番号
$serviceServerKey""サービスサーバでのTLS接続をするためのキーファイルの指定
$serviceServerCert""サービスサーバでのTLS接続をするためのサーバ証明書ファイルの指定
$serviceServerCA""サービスサーバでのTLS接続をするための中間証明書ファイルの指定
$serviceServerConnecthttp://localhostINTER-Mediatorサーバからサービスサーバに接続するためのURL
$bootWithInstalledNodefalsetrueにすると、composerではなく独自にインストールしたnodeを使ってサービスサーバを起動する
$preventSSAutoBootfalsetrueにするとサービスサーバの自動起動を行わない(利用するなら手動等での起動が必要)
$backSeconds3600 * 24 * 2クライアント同期のためのデータを保持する期間を秒数で指定する
$foreverLog-ログファイルのパス
表8-6-1 サービスサーバに関するparams.phpファイルでの設定

 サービスサーバは既定値ではオフにしています。Ver.6〜11までは、サービスサーバがオンの状態が既定値でしたが、特定の機能が必要になる時にオンにするように、Ver.11の途中で変更しました。サービスサーバを起動するには、$notUseServiceServer変数にfalseを入力します。これにより、サービスサーバが自動起動します。サービスサーバの起動は、composerでインストールしたnode.jsで行いますが、通常は、vendor/bin/nodeといったディレクトリにインストールされるはずです。このパスを考慮して自動起動するので、基本的には動くはずですが、もしその操作がうまくいっていないようなら、サーバ自体にnode.jsをインストールして、$bootWithInstalledNode変数をtrueにします。すると、単にパスにあるNode.jsを利用するようになります。

 サービスサーバ自体はnodeコマンドで起動は可能ですが、nodemonを利用して起動するようにします。nodemonはcomposerから呼び出されるnpmで自動的にインストールされます。したがって、デーモンの起動監視を行うnodeと、サービスサーバを動かすためのnodeの、2つのnodeによるプロセスが起動しているはずです。強制終了したい場合は、監視デーモン側を先に落とさないと、勝手に再起動がかかるので注意が必要です。

 サービスサーバは動作状況を標準出力に書き出します。その結果をログとして残せます。$foreverLogを省略すると、PHPのsys_get_temp_dir()関数で得られたディレクトリ以下にログファイルを残しますが、変数を絶対パスで指定するとそのファイルがログファイルになります。現在はサービスサーバの起動はnodemonを使って行っていますが、以前はforeverを使っていた名残で名前が残ってしまっていますが、今は、nodemonあるいはサービスサーバ自体の標準出力がログファイルに残るようになっています。

 もし、サービスサーバの自動起動がうまくいかないあるいはサービスサーバが別サーバで運用したいなどの理由で、サービスサーバを自動起動しない場合は、$preventSSAutoBoot変数をfalseにします。この時、サービスサーバは、nodeあるいはnodeの代わりにnodaemonを使って、以下のようにパラメータを指定することで起動します。カレントディレクトリがINTER-Mediatorのルートであるとします。

リスト8-6-1 サービスサーバを起動するコマンドとパラメータ
node src/js/ServiceServer.js ポート番号 originURL キーファイル 証明書ファイル CAファイル

 パラメータを順に解説します。ServiceServer.jsはサービスサーバのソースコードです。そして、開くポートの番号、そしてoriginURLを指定します。originURLはINTER-MediatorサーバのURLです。その後、キーファイル以降はそれぞれのパスを指定しますが、TLSを利用しないのであれば、キーファイル以降は指定は不要です。

クライアント同期を利用する場合のparams.phpでの設定

 クライアント間の同期を利用する場合、$notUseServiceServerをfalseにすると同時に、$activateClientServiceをtrueにします。ただし、これだけではクライアントとサーバが同一のホスト、つまりは開発環境でしか稼働しないでしょう。クライアント間同期を実現数するためには、INTER-Mediatorサーバ、サービスサーバ、クライアントの3つの通信が確立している必要があります。このうち、INTER-Mediatorサーバとクライアントの間では、INTER-Mediatorアプリケーションが稼働する状態であれば当然ながら接続も確立しています。INTER-Mediatorサーバとサービスサーバ間の通信は、前者が後者をコールすることが発生します。その通信のための接続先の指定は、$serviceServerConnect変数に対してフルのURLで指定をします。同一サーバならhttp://localhostで構わないですが、異なるホストの場合には、IPアドレスやホスト名を指定します。

 そして、サービスサーバとクライアントの間の通信はWebSocketを利用します。JavaScriptのライブラリであるSocketIOを利用していますが、クライアントからサービスサーバに対してコネクションが確立しなければなりません。$serviceServerProtocol、$serviceServerHost、$serviceServerPortによって、クライアントから接続するサービスサーバのURLを合成します。ここでポートは通常は使われていない大きな数字を指定しますが、何を指定すれば良いかという特に決まりはありません。すでにサーバとして使っているポートを指定した場合は、サービスサーバがエラーをログに出して、起動は行いません。起動はできたとしても、クライアントとサービスサーバが稼働しているホストが、指定したポート番号での通信ができる必要があります。うまくいかない場合は、まずはファイアウォールの設定を確認しましょう。インターネットでは通信経路にさまざまな機器が接続されていて、どこかで通信を遮断していればもちろん通信はできなくなります。

 サービスサーバとクライアントの間の通信において、TLSでの運用をしたい場合は、キーファイルや証明書ファイルの絶対パスを、それぞれの該当する変数に行います。ただし、これらのファイルは、Webサーバが実行しているユーザで読み出し権限がなくてはなりません。例えばLet’s Encryptでサーバ証明書を作ると、/etc/letsencrypt以下にファイルが作られるので、そのパスを指定すれば良いかと思うところですが、これらのファイルのうちキーファイルは、rootのみに読み書き権限がある状態になっています。それではアクセス権を変えてと思うかもしれませんが、それはセキュリティ上のリスクになります。Apacheはrootユーザでこれらのファイルを読み込むのですが、Node.jsでサーバを運用する場合はそうしたroot処理はありません。結果的に、キーファイルなどを含めて必要なファイルを別途どこかWeb公開していないような安全な場所にコピーして、そこの絶対パスをparams.phpファイルの変数に指定するのが順当と思われます。Let's Encryptの場合だと自動的に3ヶ月に1度証明書は更新されますが、スクリプトを組んで更新された証明書を別ディレクトリにコピーするなどして運用する必要が出てくるでしょう。

 クライアント間同期が始まると、どのクライアントにどのテーブルのどのレコードが表示されているかをデータベース上のテーブルで管理するようにします。ページを正しく閉じればその情報も消されますが、Webブラウザの場合は正しく手順通りに閉じてくれない場合も発生します。そこで、$backSeconds変数に指定した秒数が経過すると、同期のための情報はクリアするように動作します。例えば、1日あるいは数日後に消えるというのは順当な設定かと思われます。

このセクションのまとめ

 サービスサーバは、クライアント間同期を使うには、起動する必要がありますが、params.phpファイルを指定することで、基本的に自動的に起動します。