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

レコードの検索方法 - FX.php

まずはFX.phpをベースに、検索と一覧画面を実装する。FileMaker API for PHPでできることを極力FX.phpでも実現するということで、いくつか代替案をもちいて実装をおこなった。このサンプルは「コントローラ」「検索+一覧画面」「エラー画面」の3ファイルから構成している。

コントローラ - fx_find.php

<?php

include_once('../fx/FX.php');
include_once('../fx/FX_Fuzzy_Debugger.php');

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

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

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

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

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

// 2. SetDBDataで使用するDB、レイアウト、一度に取得するレコード件数、レスポンスレイアウトを指定
$data->SetDBData('fmapi_test','fmapi_list', $max);
$data->SetDBUserPass('admin','admin');

$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');

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

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

// 5. FMSkipRecordsで先頭から除外するレコード件数を指定
$data->FMSkipRecords($skip);

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

// 7. FMFindまたはDoFXAction('find')でリクエスト発行
// 8. 結果を配列で取得、$resultSetに格納
$resultSet = $data->FMFind();
// $resultSet = $data->DoFXAction('find'); でも可

// 9. $resultSet['errorCode']の値でエラー判定
if ( 0 === (int)$resultSet['errorCode'] )
{
    include_once('./fx_find_view.php');
}
else
{
    // エラー処理
    include_once('./fx_find_error.php');
}
?>

検索+一覧画面 - fx_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 FX.php 検索</title>
<style type="text/css">
<!--
table th
{
    color: #000000;
    background-color: #cccccc;
}
-->
</style>
</head>

<body>

<form action="./fx_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
        foreach( $data->lastFields as $fieldValue )
        {
            // タイプが「テキスト」「数字」「日付」「オブジェクト」のみを表示
            switch($fieldValue['type'])
            {
                case 'TEXT':
                case 'NUMBER':
                case 'DATE':
                    echo '<th><a href="./fx_find.php?ft_text=' . h($_GET['ft_text']).
                         '&sortField=' . $fieldValue['name'] .
                         '&sortOrder=' . $order . '">' . $fieldValue['name'] . '</a></th>';
                    break;
                case 'CONTAINER':
                    echo '<th>' . $fieldValue['name'] . '</th>';
                    break;
                default:
                    break;
            }
        }
        unset($fieldValue);
        ?>
    </tr>
    <?php
    // 10. $resultSet['data']からフィールド内容を取得
    foreach( $resultSet['data'] as $recordValue )
    {
        ?>
        <tr>
        <?php
        foreach( $data->lastFields as $fieldValue )
        {
            if (!empty($recordValue[$fieldValue['name']][0]))
            {
                switch($fieldValue['type'])
                {
                    case 'TEXT':
                        echo '<td>' . h($recordValue[$fieldValue['name']][0]) .  '</td>';
                        break;
                    case 'NUMBER':
                        echo '<td align="right">' . h(number_format($recordValue[$fieldValue['name']][0])) .  '</td>';
                        break;
                    case 'DATE':
                        echo '<td align="center">' . date('Y/m/d', strtotime($recordValue[$fieldValue['name']][0])) . '</td>';
                        break;
                    case 'CONTAINER':
                        echo '<td align="center"><img src="' . h($recordValue[$fieldValue['name']][0]) . '"></td>';
                        break;
                    default:
                        break;
                }
            }
            else
            {
                echo '<td>&nbsp;</td>';
            }
        }
        ?>
        </tr>
        <?php
    }
    unset($recordValue);
    ?>
</table>

<?php
echo '<p>' . number_format($resultSet['foundCount']) . '件中、' .
     number_format($skip+1) . '~' . number_format($max) . '件を表示</p>';
echo '<ul>';
if (!empty($resultSet['linkPrevious']))
{
    echo '<li><a href="' . h($resultSet['linkPrevious']) . '">前の' . $max . '件を表示</li>';
}
if (!empty($resultSet['linkNext']))
{
    echo '<li><a href="' . h($resultSet['linkNext']) . '">次の' . $max . '件を表示</li>';
}
echo '</ul>';
?>

</body>

</html>

エラー画面 - fx_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 FX.php 検索 - エラー</title>
<style type="text/css">
<!--
table th
{
    color: #000000;
    background-color: #cccccc;
}
-->
</style>
</head>

<body>

<form action="./fx_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['errorCode']); ?></td>
    </tr>
    <tr>
        <th>エラー内容</th>
        <td><?php echo h($errorsList[$resultSet['errorCode']]); ?></td>
    </tr>
</table>

</body>

</html>

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

fx_find.phpにアクセスすると検索+一覧画面(fx_find_view.php)が表示される。画面上部に検索UI、下部はレコードの一覧とページャになっている

「次の10件を表示」をクリックすると、次のページへ移動する

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

ft_dateに「2010/1/1」と入力して検索した結果。FX.phpにはFileMaker API for PHPのsetFieldFromTimestampのような関数が用意されていないので、ここではstrtotimeをもちいて実現している

検索条件にマッチしたレコードが見つからなかった場合は、エラー画面(fx_find_error.php)が表示される

それでは実装のポイントを追って解説していこう。まずは手順ベースでのポイントだ。

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

$data = new FX('localhost', 80, 'FMPro9', 'http');

まずはnew FXでインスタンスを作成する。コンストラクタの引数は「サーバ接続先アドレス」「ポート番号」「データタイプ」「スキーマ」。接続先は自分のマシンなので、ここではlocalhost, 80, FMPro9, httpと指定した。

余談になるが、データタイプには次の文字列を指定することができる。

  • fmpro5
  • fmpro6
  • fmpro5/6
  • fmpro7
  • fmpro8
  • fmpro9
  • openbase
  • mysql
  • postgres
  • odbc
  • cafephp4pc

FileMakerをDBとして使う感覚でmysqlやpostgresを指定するとfoundCountや-max/-skipの値をうまく処理してくれず、なかなか期待した動作になってくれないことがおおい。しかしFX.phpはオープンソースソフトウェア。ソースコードを読み、自分で修正を加えることでさまざまな応用が可能になっている。このあたりは難読化が施されているFileMaker API for PHPと比較すると大きなメリットだ。

2. SetDBDataで使用するDB、レイアウト、一度に取得するレコード件数、レスポンスレイアウトを指定

$data->SetDBData('fmapi_test','fmapi_list', $max);

SetDBDataの引数順は「データベース名」「レイアウト名」「一度に取得するレコード数」「レスポンスレイアウト」。ここでは「fmapi_test.fp7のfmapi_listレイアウトに対して処理を実行、一度に取得するレコード件数は$max(=10)」という意味となる。

3. AddDBParamまたはAddParamArrayで検索したい内容を指定

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

ここでのポイントは3点。

  • 検索文字列が指定された場合のみ、はじめて検索条件を指定するように
  • テキストタイプのフィールド検索は前後に「*」をつけ、FileMaker側で部分一致検索となるように
  • 日付タイプのフィールド検索はdateとstrtotimeを使い、MM/DD/YYYY形式に変換しなおしてから検索するように

検索文字列を入力した段階ではじめてAddDBParamとし、余計な検索をおこなわないように。また、テキストタイプのフィールド検索は前後に「_」をつけて「_FileMaker*」といった部分一致検索をおこなっている。

FileMaker API for PHPにはUNIXタイムスタンプから日付をセットするsetFieldFromTimestampが用意されているのであまり問題にはならないが、FX.phpにはこれに相当する機能がない。日本での日付入力はYYYY/MM/DD形式が一般的だが、この日付はFileMaker Web公開ではそのまま利用できないので、「date('m/d/Y' ,strtotime($_GET['ft_date']))」とすることでMM/DD/YYYY形式に変換してから検索をおこなうようにしている。

4. AddSortParamでソート順を指定

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

ここでのポイントは2点

  • 指定したフィールドのみ、ソートがおこなわれるように
  • ソートが指定されていない場合は、ソートをおこなわない

FileMakerのソートは他データベースと比較すると、かなりパフォーマンスが劣る。FileMaker単体で利用する場合はさほど気にならないが、サーバ公開されたファイルやWeb公開で利用する場合は無視できない差となる。そこでここでは、初期状態ではソートを実行しないようにしている。エンドユーザの要望にも左右されるところだが、極力ソートはおこなわないほうが吉だろう。

5. FMSkipRecordsで先頭から除外するレコード件数を指定 ~ 8. 結果を配列で取得

$data->FMSkipRecords($skip);
$resultSet = $data->FMFind();

今回はポータルを利用しないので、AddDBParam('-relatedsets.filter')/AddDBParam('-relatedsets.max')は使用せず。$resultSetに結果セットの配列を格納している。

9. $resultSet['errorCode']の値でエラー判定

if ( 0 === (int)$resultSet['errorCode'] )

$resultSet'errorCode']にはFileMakerエラーコードが返るので、これを使用してエラー処理。ここでは「0: エラーなし」とそれ以外で分岐させている。エラーコードごとに処理を実装する場合は別途[対応表を参考にしてほしい。

10. $resultSet['data']からフィールド内容を取得

    foreach( $resultSet['data'] as $recordValue )

$resultSet['data']配列からレコード情報を取り出し、ループでテーブルセルを生成している。

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

ソート指定の切替

$order = ( 'descend' === $_GET['sortOrder'] ) ? 'ascend' : 'descend';

一覧ヘッダのリンクをクリックするごとに、ソート順の指定が昇順/降順と切り替わるようになる。

一覧表示部の自動生成

FileMaker API for PHPではレイアウトに配置されているフィールド情報を柔軟に取得できる。FX.phpにはこれらのメソッドはないものの、最後にアクセスしたレイアウトのフィールド情報が$data->lastFieldsに格納されるので、これを利用している。$data->lastFieldsに格納される情報は次のとおり。

キー 内容
emptyok string 空欄不可か否かが格納される。空欄不可の場合は「NO」が、空欄許可の場合は「YES」が格納される
maxrepeat string 最大繰り返し数が格納される
name string フィールド名が格納される
type string フィールドタイプが格納される。値はそれぞれ「テキスト(TEXT)」「数字(NUMBER)」「日付(DATE)」「時刻(TIME)」「タイムスタンプ(TIMESTAMP)」「オブジェクト(CONTAINER)」となる
extra string FileMakerデータベース以外の場合、プライマリキーやオートインクリメントといった情報が格納される

fx_find_view.phpではこの$data->lastFieldsのname・typeを利用し、指定したタイプだけのフィールド列、タイプごとに中央/右寄せやイメージタグを含めたレコード一覧を自動生成している。

ページャの生成

FX.phpでは結果セットのなかに、対象レコード数や次・前ページ用のリンクが格納されている。

キー 内容
foundCount int 対象レコード数
linkPrevious string 前ページ移動用のリンクが格納される。存在しない場合は空欄に
linkNext string 次ページ移動用のリンクが格納される。存在しない場合は空欄に
echo '<p>' . number_format($resultSet['foundCount']) . '件中、' .
     number_format($skip+1) . '~' . number_format($max) . '件を表示</p>';
echo '<ul>';
if (!empty($resultSet['linkPrevious']))
{
    echo '<li><a href="' . h($resultSet['linkPrevious']) . '">前の' . $max . '件を表示</li>';
}
if (!empty($resultSet['linkNext']))
{
    echo '<li><a href="' . h($resultSet['linkNext']) . '">次の' . $max . '件を表示</li>';
}
echo '</ul>';

画面下部のページャではこれら値とリンクの有無をチェックしてページャの表示切り替えをおこなっている。

次回はFileMaker API for PHPを使った実装例を紹介しよう。