Webアプリケーションを実装するにあたり、コード中でもっとも登場回数がおおくなると思われるレコード検索。実用的なサンプルをもとに、FileMaker API for PHPでの実装例を紹介する。FX.phpとの実装差を比較してもらいたい。

レコードの検索方法 - FileMaker API for PHP

今回はFileMaker API for PHPをベースに、検索と一覧画面を実装する。前回と同様、FX.phpでできることをFileMaker API for PHPでも実現するということコンセプトで、代替案をもちいて実装をおこなっている。ファイル構成はFX.phpでの実装同様「コントローラ」「検索+一覧画面」「エラー画面」としている。

コントローラ - api_find.php

<?php

include_once('../FileMaker.php');

// 文字列エスケープ用関数
function h($string)
{
    return htmlspecialchars(trim($string), ENT_QUOTES, 'UTF-8');
}

// 一度に取得する件数を指定
$max = 10;

// 先頭から除外するレコード数を取得
$skip = ( 0 < (int)$_GET['skip']) ? (int)$_GET['skip'] : 0;

// ソート指定を取得
$order = ( FILEMAKER_SORT_DESCEND === $_GET['sortOrder'] ) ? FILEMAKER_SORT_ASCEND : FILEMAKER_SORT_DESCEND;

// 1. new FileMakerでFileMakerのインスタンスを作成、接続先を指定
$data = new FileMaker('fmapi_test', 'http://localhost:80', 'admin', 'admin');

// 2. newFindCommandでFileMaker_Command_Findオブジェクトを作成。このときレイアウトを指定
$findCommand = $data->newFindCommand('fmapi_list');

// 3. addFindCriterionで検索したい内容を指定
if (!empty($_GET['ft_serial']))
{
    $findCommand->addFindCriterion('ft_serial', $_GET['ft_serial']);
}
if (!empty($_GET['ft_text']))
{
    $findCommand->addFindCriterion('ft_text', '*' . $_GET['ft_text'] . '*' );
}
if (!empty($_GET['ft_date']))
{
    $findCommand->addFindCriterion('ft_date', date('m/d/Y' ,strtotime($_GET['ft_date'])));
}
if (!empty($_GET['ft_num']))
{
    $findCommand->addFindCriterion('ft_num', $_GET['ft_num']);
}

// 4. addSortRuleでソート順を指定
switch($_GET['sortField'])
{
    case 'ft_serial':
    case 'ft_text':
    case 'ft_date':
    case 'ft_num':
    case 'ft_repetitionText':
        $data->addSortRule($_GET['sortField'], 1, $order);
        break;
    default:
        // ソートフィールドが指定されなかった場合は、ソートをおこなわない
        break;
}

// 5. setRangeで先頭から除外するレコード件数・一度に取得するレコード件数を指定
$findCommand->setRange($skip, $max);

// 6. レイアウトにポータルが配置されている場合は、
// setRelatedSetsFiltersでフィルタリングの設定をおこなう

// 7. setResultLayoutでレスポンスレイアウトを指定

// 8. executeでリクエスト発行
// 9. 成功した場合はFileMaker_Resultオブジェクトが、失敗した場合はFileMaker_Errorオブジェクトが返る
$resultSet = $findCommand->execute();

// 10. FileMaker::isErrorでエラー判定
if (!FileMaker::isError($resultSet))
{
    include_once('./api_find_view.php');
}
else
{
    // エラー処理

    // ロケールをjaとセットしておくことで、エラーメッセージを日本語にできる
    $data->setProperty('locale', 'ja');
    include_once('./api_find_error.php');
}
?>

検索+一覧画面 - api_find_view.php

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>FileMaker x FileMaker API for PHP 検索</title>
<style type="text/css">
<!--
table th
{
    color: #000000;
    background-color: #cccccc;
}
-->
</style>
</head>

<body>

<form action="./api_find.php" method="get">
    <table border="1">
        <tr>
            <th>ft_serial</th>
            <th>ft_text</th>
            <th>ft_date</th>
            <th>ft_num</th>
        </tr>
        <tr>
            <td><input type="text" name="ft_serial" value="<?php echo h($_GET['ft_serial']); ?>"></td>
            <td><input type="text" name="ft_text" value="<?php echo h($_GET['ft_text']); ?>"></td>
            <td><input type="text" name="ft_date" value="<?php echo h($_GET['ft_date']); ?>"></td>
            <td><input type="text" name="ft_num" value="<?php echo h($_GET['ft_num']); ?>"></td>
        </tr>
        <tr>
            <td colspan="4" align="center"><input type="submit" value="検索"></td>
        </tr>
    </table>
</form>

<br>

<table border="1">
    <tr>
        <?php
        $layoutObj = $resultSet->getLayout();
        foreach( $layoutObj->getFields() as $fieldObj )
        {
            // タイプが「テキスト」「数字」「日付」「オブジェクト」のみを表示
            switch($fieldObj->getResult())
            {
                case 'text':
                case 'number':
                case 'date':
                    echo '<th><a href="./api_find.php?ft_text=' . h($_GET['ft_text']).
                         '&sortField=' . $fieldObj->getName() .
                         '&sortOrder=' . $order . '">' . $fieldObj->getName() . '</a></th>';
                    break;
                case 'container':
                    echo '<th>' . $fieldObj->getName() . '</th>';
                    break;
                default:
                    break;
            }
        }
        unset($fieldObj);
        ?>
    </tr>
    <?php
    // 11. getFirstRecordやgetRecordsでFileMaker_Recordオブジェクトを取得
    foreach( $resultSet->getRecords() as $recordObj )
    {
        ?>
        <tr>
        <?php
        foreach( $layoutObj->getFields() as $fieldObj )
        {
            if ( '' !== $recordObj->getField($fieldObj->getName()) )
            {
                switch($fieldObj->getResult())
                {
                    case 'text':
                        echo '<td>' . h($recordObj->getFieldUnencoded($fieldObj->getName())) .  '</td>';
                        break;
                    case 'number':
                        echo '<td align="right">' . h(number_format($recordObj->getFieldUnencoded($fieldObj->getName()))) .  '</td>';
                        break;
                    case 'date':
                        echo '<td align="center">' . date('Y/m/d', $recordObj->getFieldAsTimestamp($fieldObj->getName())) . '</td>';
                        break;
                    case 'container':
                        echo '<td align="center"><img src="' . h($recordObj->getFieldUnencoded($fieldObj->getName())) . '"></td>';
                        break;
                    default:
                        break;
                }
            }
            else
            {
                echo '<td>&nbsp;</td>';
            }
        }
        ?>
        </tr>
        <?php
    }
    unset($recordObj);
    ?>
</table>

<?php
echo '<p>' . number_format($resultSet->getFoundSetCount()) . '件中、' .
     number_format($skip+1) . '~' . number_format($skip+$max) . '件を表示</p>';
echo '<ul>';
$linkPrevious = ( $max === (int)( $skip + $max ) ) ?
            '' :
            $_SERVER['SCRIPT_NAME'] . '?skip=' . (int)( $skip - $max ). '&' . preg_replace('/skip=[\d]*[&]?/', '', $_SERVER['QUERY_STRING'] );
if (!empty($linkPrevious))
{
    echo '<li><a href="' . h($linkPrevious) . '">前の' . $max . '件を表示</li>';
}
$linkNext = ( (int)$resultSet->getFoundSetCount() === (int)( $skip + $max ) ) ?
            '' :
            $_SERVER['SCRIPT_NAME'] . '?skip=' . (int)( $skip + $max ). '&' . preg_replace('/skip=[\d]*[&]?/', '', $_SERVER['QUERY_STRING'] );
if (!empty($linkNext))
{
    echo '<li><a href="' . h($linkNext) . '">次の' . $max . '件を表示</li>';
}
echo '</ul>';
?>

</body>

</html>

エラー画面 - api_find_error.php

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>FileMaker x FileMaker API for PHP 検索 - エラー</title>
<style type="text/css">
<!--
table th
{
    color: #000000;
    background-color: #cccccc;
}
-->
</style>
</head>

<body>

<form action="./api_find.php" method="get">
    <table border="1">
        <tr>
            <th>ft_serial</th>
            <th>ft_text</th>
            <th>ft_date</th>
            <th>ft_num</th>
        </tr>
        <tr>
            <td><input type="text" name="ft_serial" value="<?php echo h($_GET['ft_serial']); ?>"></td>
            <td><input type="text" name="ft_text" value="<?php echo h($_GET['ft_text']); ?>"></td>
            <td><input type="text" name="ft_date" value="<?php echo h($_GET['ft_date']); ?>"></td>
            <td><input type="text" name="ft_num" value="<?php echo h($_GET['ft_num']); ?>"></td>
        </tr>
        <tr>
            <td colspan="4" align="center"><input type="submit" value="検索"></td>
        </tr>
    </table>
</form>

<br>

<table border="1">
    <tr>
        <th>FileMakerエラーコード</th>
        <td><?php echo h($resultSet->getCode()); ?></td>
    </tr>
    <tr>
        <th>エラー内容</th>
        <td><?php echo h($resultSet->getMessage()); ?></td>
    </tr>
</table>

</body>

</html>

レイアウトに配置されているフィールドのうち、タイプが「テキスト」「数字」「日付」「オブジェクト」のフィールドのみを一覧表示する。Webブラウザでapi_find.phpにアクセスする。

api_find.phpにアクセスすると検索+一覧画面(api_find_view.php)が表示される。画面表示はFX.phpとまったく一緒だが、中身のコーディングは上記ソースコードのとおりかなり異なる

「次の10件を表示」をクリックすると、次のページへ移動する。APIにはページ遷移リンクの自動生成機能がないので自作する必要あり

ft_textに「filemaker」と入力して検索した結果

ft_serialにテキスト文字列を入力して検索した結果。エラー画面(api_find_error.php)が表示される。エラー内容はロケールを設定することで日本語で表示することが可能

それではFX.phpと同様、実装のポイントを追って解説していこう。興味がある方は随所でFX.phpとの実装を比較してみてほしい。まずは手順ベースでのポイントだ。

1. new FileMakerでFileMakerのインスタンスを作成、接続先を指定

$data = new FileMaker('fmapi_test', 'http://localhost:80', 'admin', 'admin');

まずはnew FileMakerでFileMakerインスタンスを作成する。コンストラクタの引数は「データベース名」「ポート番号をふくめたサーバ接続先アドレス」「ファイルを開くためのID」「ファイルを開くためのパスワード」。慣れないうちはFX.phpのコンストラクタ・SetDBDataと混同しやすいので注意が必要だ。

2. newFindCommandでFileMaker_Command_Findオブジェクトを作成。このときレイアウトを指定

$findCommand = $data->newFindCommand('fmapi_list');

FileMaker API for PHPでは検索条件もオブジェクトで管理する。FileMaker_Command_Findオブジェクトを作成し、レイアウトを指定。

3. addFindCriterionで検索したい内容を指定

if (!empty($_GET['ft_serial']))
{
    $findCommand->addFindCriterion('ft_serial', $_GET['ft_serial']);
}
if (!empty($_GET['ft_text']))
{
    $findCommand->addFindCriterion('ft_text', '*' . $_GET['ft_text'] . '*' );
}
if (!empty($_GET['ft_date']))
{
    $findCommand->addFindCriterion('ft_date', date('m/d/Y' ,strtotime($_GET['ft_date'])));
}
if (!empty($_GET['ft_num']))
{
    $findCommand->addFindCriterion('ft_num', $_GET['ft_num']);
}

2で作成したFileMaker_Command_Findオブジェクトに、検索条件を指定する。3点のポイントはFX.phpと同じなので省略。FX.phpのAddDBParamがaddFindCriterionに置きかわったくらいだ。FileMaker API for PHPにはAddParamArrayに相当する機能がないので注意。検索オブジェクトを複数作成すれば複合検索も簡単に実装できる。

4. addSortRuleでソート順を指定

switch($_GET['sortField'])
{
    case 'ft_serial':
    case 'ft_text':
    case 'ft_date':
    case 'ft_num':
    case 'ft_repetitionText':
        $data->addSortRule($_GET['sortField'], 1, $order);
        break;
    default:
        // ソートフィールドが指定されなかった場合は、ソートをおこなわない
        break;
}

「指定したフィールドのみ、ソートがおこなわれるように」「ソートが指定されていない場合は、ソートをおこなわない(初期状態ではソートを実行しないように)」というポイントは同じ。FX.phpと違う箇所は、先頭で$orderに格納する値にFileMaker API for PHP側で用意されている定数を利用する点。

$order = ( FILEMAKER_SORT_DESCEND === $_GET['sortOrder'] ) ? FILEMAKER_SORT_ASCEND : FILEMAKER_SORT_DESCEND;

ascend, descendと直書きしても動作する。しかしFileMaker Serverのバージョンアップなどで、Web公開に関連する文法の仕様変更が発生する可能性を考えると、用意されている定数を利用しておくのが吉だろう。

5. setRangeで先頭から除外するレコード件数・一度に取得するレコード件数を指定 ~ 8. executeでリクエスト発行

$findCommand->setRange($skip, $max);
$resultSet = $findCommand->execute();

レスポンスレイアウトとポータルを利用しないので、setRelatedSetsFilters/setResultLayoutは使用せず。検索処理に成功した場合はFileMaker_Resultオブジェクトが、失敗した場合はFileMaker_Errorオブジェクトが$resultSetに返る。

10. FileMaker::isErrorでエラー判定

if (!FileMaker::isError($resultSet))

FX.phpではXMLにふくまれるFileMakerエラーコード(errorCode)を利用してエラー処理を実装した。FileMaker API for PHPでは専用のメソッド「FileMaker::isError」が用意されているので、これを利用してエラー処理を実装する。

11. getFirstRecordやgetRecordsでFileMaker_Recordオブジェクトを取得

foreach( $resultSet->getRecords() as $recordObj )

FileMaker_Resultオブジェクト$resultSetからgetRecord()でレコード情報を取り出し、ループでテーブルセルを生成。ループ中のフィールド内容の取得方法など、こまかい使い勝手が異なる。

このほかのポイントは次のとおり。

エラーメッセージの日本語化

FileMaker API for PHPではあらかじめ多言語のエラーメッセージが用意されている。ロケールに"ja"設定することで日本語のエラーメッセージを表示することが可能だ。

$data->setProperty('locale', 'ja');

指定しない場合は自動的に英語ロケールとなる。

一覧表示部の自動生成

FileMaker API for PHPではレイアウトに配置されているフィールド情報を柔軟に取得できる。FileMaker_Resultオブジェクトからまずレイアウト情報(FileMaker_Layout object)を抜き出し、フィールド情報(FileMaker_Field object)を抜き出す。取得できる情報は次のとおり(一部を抜粋)。

メソッド名 内容
getLocalValidationRulesなど 入力値の制限情報
getRepetitionCount 最大繰り返し数
getName フィールド名
getResult フィールドタイプ。値はそれぞれ「テキスト(text)」「数字(number)」「日付(date)」「時刻(time)」「タイムスタンプ(timestamp)」「オブジェクト(container)」となる。FX.phpと異なりすべて小文字で返るので注意
getType 対象フィールドが計算フィールドか集計フィールドかを判定する。計算フィールドの場合は「calculation」、集計フィールドの場合は「summary」、それ以外の場合は「normal」となる
getStyleType フィールドコントロールスタイル/表示形式
isAutoEntered 入力値の自動化が設定されているか否かを判定
isGlobal グローバルフィールドか否かを判定

ここではgetName, getResultを利用し、指定したタイプだけのフィールド列、タイプごとに中央/右寄せやイメージタグを含めたレコード一覧を自動生成している。1度のリクエストでレイアウト・フィールド詳細情報が取得できるのはFileMaker API for PHPの強みの一つだ。FileMaker_Fieldクラス・オブジェクトの詳細はバックナンバー「フィールド情報の取得に特化したクラス、FileMaker_Field」を参照してほしい。

ページャの生成

FX.phpでは結果セットのなかに、対象レコード数や次・前ページ用のリンクが格納されている。残念ながらこの機能はFileMaker API for PHPには用意されていないので、各種リンクは自前で用意する必要がある。

echo '<p>' . number_format($resultSet->getFoundSetCount()) . '件中、' .
     number_format($skip+1) . '~' . number_format($skip+$max) . '件を表示</p>';
echo '<ul>';
$linkPrevious = ( $max === (int)( $skip + $max ) ) ?
            '' :
            $_SERVER['SCRIPT_NAME'] . '?skip=' . (int)( $skip - $max ). '&' . preg_replace('/skip=[\d]*[&]?/', '', $_SERVER['QUERY_STRING'] );
if (!empty($linkPrevious))
{
    echo '<li><a href="' . h($linkPrevious) . '">前の' . $max . '件を表示</li>';
}
$linkNext = ( (int)$resultSet->getFoundSetCount() === (int)( $skip + $max ) ) ?
            '' :
            $_SERVER['SCRIPT_NAME'] . '?skip=' . (int)( $skip + $max ). '&' . preg_replace('/skip=[\d]*[&]?/', '', $_SERVER['QUERY_STRING'] );
if (!empty($linkNext))
{
    echo '<li><a href="' . h($linkNext) . '">次の' . $max . '件を表示</li>';
}
echo '</ul>';

上記のリンク生成式はFX.phpの内部関数、BuildLinkQueryStringやAssembleDataSetでおこなっているものを簡略化したものだ。

一見多機能なFileMaker API for PHPだが、サーバにかかる負荷やパフォーマンス面で気をつけるべき点も多い。次回はFX.phpとFileMaker API for PHPの負荷計測・パフォーマンス測定結果を紹介しよう。