前回はWebアプリケーション側からオブジェクトフィールドを表示する方法を紹介した。オブジェクトフィールドにはバイナリファイルを格納することができるが、Web公開エンジンからは一切の変更や削除ができないといったデメリットがある。今回は「オブジェクトフィールドを使用しない」前提で画像といったバイナリファイルを管理・操作する方法を紹介しよう。

オブジェクトフィールドから脱却することでのメリットとは

オブジェクトフィールドはFileMaker Pro上では取りあつかいやすい形式だ。しかしWebアプリケーションから利用する前提だと前回でもすこし触れたとおり、いくつか扱いにくい点がある。オブジェクトフィールドを使用するにおいて、注意するべき点をまとめてみよう。

  • Web公開からは参照のみで、トリッキーな方法を使わないかぎりフィールド内容の変更や削除は一切できない
  • サイズの大きなファイルをオブジェクトフィールドに格納すると、リクエスト~処理完了までに時間がかかる。サーバにかかる負荷も大きい
  • FileMakerファイルのサイズが肥大しやすくなる。レコードが数万、ファイルサイズが数100M~1G以上のデータベースになってくると、有事の際の修復といったメンテナンス作業に時間がかかるようになってしまう

とくに最後の「ファイルサイズが肥大しやすくなる」は運用面でトラブルになりがちだ。開発中はとくに気にならないが「実際に運用をはじめると思わぬサイズになっていた」というのはよくある話。FileMakerファイルが破損してしまった場合、ファイルの修復にかかる時間はファイルサイズが大きければ大きいほど長くなる。運用中のシステムにおいてファイルの修復・復帰に数時間以上かかるのは致命的だ。

筆者はとくに目立った理由がなければ、オブジェクトフィールドは使わないほうが良いと結論づけている。オブジェクトフィールドを使わずに、FileMakerには格納したいバイナリへのパスだけを保存しておくようにしておく。こうすることでバイナリを直接FileMakerファイルに格納しなくなるので、FileMakerファイルのサイズを抑えることができる。Webアプリケーション側からバイナリを参照するときも、パス情報をもとにPHP側で取得すればいいだけ。FileMakerにはパス情報しか持たせていないので、ファイルアップロードのロジックさえ組んでしまえば擬似的にFileMakerにバイナリを持たせることも可能になるのだ。

前置きが長くなってしまったが、これらを意識して実装をしてみよう。

1つのオブジェクトフィールドを、テキストフィールドと計算フィールドに

おおまかには、1つのオブジェクトフィールドをテキストフィールドと計算フィールドの2つに分割するように設計する。

  • テキストフィールド: ファイルへのパス、またはファイル名を格納する
  • 計算フィールド: フィールドタイプは「オブジェクト」に設定。テキストフィールドを使用し、ファイルの中身を表示させる

このほかにも「ファイルの保存先パス情報」を格納するグローバルフィールドがあってもいだろう。なお、計算フィールドはFileMaker Proで構築するUI用に用意するものだ。Webアプリケーションからしか利用しない場合は必要ない。

それでは前回使用したファイルを意識して実装してみよう。用意したフィールドは次のとおり。なお、サンプルをわかりやすくするために格納するファイルは画像だけに限定している。

オブジェクトフィールドを使用する場合

オブジェクトフィールドを使用しない場合。計算フィールドの計算結果はオブジェクトとして返すように設定している

FileMaker Pro上での計算フィールドの見え方。オブジェクトフィールドを使用する場合と比較しても、見た目の差は皆無だ

FileMaker Pro上での画像の表示は計算フィールドを使用する。Webアプリケーションからファイルの内容を表示したい場合は、テキストフィールドを使用する。

fm_view_obj.php

<?php

// 外部ファイルにて SYSTEM_ATTACH_PATH を定義しています
// define('SYSTEM_ATTACH_PATH', './attached/'); // 画像の保存先

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

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

$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_obj', 1);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->SetRecordID(1);
$dataSet = $data->FMFind();

?>

<!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">
<title>FX.php ファイル参照</title>
</head>

<body>

    <table>

        <?php
        for ( $n = 1 ; $n < 4; $n++ )
        {
        ?>
        <tr>
            <th>
                画像(<?php echo $n; ?>)
            </th>
            <td align="left">
                <img src="<?php echo SYSTEM_ATTACH_PATH . h($dataSet['data'][key($dataSet['data'])]['fo_objPath_'.$n][0]); ?>"
                    alt="画像(<?php echo $n; ?>)のサムネイル" border="1">
            </td>
        </tr>
        <?php
        }
        ?>
    </table>

</body>

</html>

テキストフィールド内のファイル名を<img>タグのsrcに使用している。実際にWebブラウザでアクセスしてみよう。

FileMaker内で管理しているファイルが表示された。オブジェクトフィールドと違って何回もFileMakerへのリクエストが発生しないぶん、サーバへかかる負荷もすくない

FileMakerファイルからバイナリファイルそのものを取り出してはいないので、/fmi/xml/cnt/data.jpgへのリクエスト回数も抑えることが可能だ。サーバにかかる負荷のことも考えると、オブジェクトフィールドを使用しない実装のほうがメリットが大きいだろう。続けてファイルアップローダの実装例を紹介しよう。

fm_edit - 編集画面

<?php

// 外部ファイルにて SYSTEM_ATTACH_PATH を定義しています
// define('SYSTEM_ATTACH_PATH', './attached/'); // 画像の保存先

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

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

$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_obj', 1);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->SetRecordID(1);
$dataSet = $data->FMFind();

// URI文字列を格納
?>
<!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">
<title>FX.php ファイルの格納</title>

</head>

<body>

    <form action="./fm_file_upload.php" accept-charset="utf-8" enctype="multipart/form-data" method="post">
        <table>
            <?php
            for ( $n = 1 ; $n < 4; $n++ )
            {
            ?>
            <tr>
                <th>
                    画像(<?php echo $n; ?>)
                </th>
                <td align="left">
                    <img src="<?php echo SYSTEM_ATTACH_PATH . h($dataSet['data'][key($dataSet['data'])]['fo_objPath_'.$n][0]); ?>"
                        alt="画像(<?php echo $n; ?>)のサムネイル" width="200" height="150" border="1">
                    <br>
                    <input name="photo<?php echo $n; ?>_file" type="file">
                </td>
            </tr>
            <?php
            }
            ?>
        </table>

        <p>
            <input type="submit" value="保存">
        </p>

    </form>

</body>

</html>

fm_file_upload - ファイルアップロード処理

<?php

// 外部ファイルにて SYSTEM_ATTACH_PATH を定義しています
// define('SYSTEM_ATTACH_PATH', './attached/'); // 画像の保存先

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

// 既存のレコード情報を取得
$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_obj', 1);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->SetRecordID(1);
$dataSet = $data->FMFind();

for ($n = 1 ; $n < 4; $n++)
{
    if (UPLOAD_ERR_OK === $_FILES['photo' . $n . '_file']['error'])
    {
        // (1) アップロードされたファイルをリネームし、SYSTEM_ATTACH_PATH 以下に保存
        $tmp['photo'][$n]['filename'] =
            date('YmdHis') .'_' . $n . '_'.
            $_FILES['photo' . $n . '_file']['name'];
        move_uploaded_file
        (
            $_FILES['photo' . $n . '_file']['tmp_name'],
            SYSTEM_ATTACH_PATH.
            $tmp['photo'][$n]['filename']
        );

        // (2) 既存のファイルを削除
        if (file_exists(SYSTEM_ATTACH_PATH.$dataSet['data'][key($dataSet['data'])]['fo_objPath_'.$n][0]))
        {
            unlink(SYSTEM_ATTACH_PATH.$dataSet['data'][key($dataSet['data'])]['fo_objPath_'.$n][0]);
        }
    }

}

// (3) FileMakerにパス情報を保存
$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_obj', 1);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
for ( $n = 1 ; $n < 4; $n++ )
{
    if (!empty($tmp['photo'][$n]['filename']))
    {
        $data->AddDBParam('fo_objPath_'.$n , $tmp['photo'][$n]['filename']);
    }
}
$data->SetRecordID(2);
$dataSet = $data->FMEdit();

if ( 0 === (int)$dataSet['errorCode'] )
{
    include_once('./fm_view_obj.php');
}
else
{
    // エラー処理..
}

ファイルのアップロードに成功した場合にのみ、「アップロードされたファイルをリネームし、特定のディレクトリ以下に保存」「既存のファイルを削除」「FileMakerにパス情報を保存」をおこなう。

画像(1)のみ、あたらしい画像ファイルを選択した"

ファイルのアップロード後。既存のファイル「image_1.jpg」が削除され、アップロードしたファイル「image_4.gif」がタイムスタンプつきで追加された

FileMaker側でもWebアプリケーション側でも問題なく画像が表示できている

Webアプリケーション側からも、FileMaker内で管理するファイルの操作が可能となった。FileMaker Proから直感的に操作ができるオブジェクトフィールドのメリットはなくなってしまうが、紹介したとおりそれ以上のメリットが得られると感じてもらえたかと思う。オブジェクトフィールドを使う/使わないは一概にはどちらが秀でてるとは言えないが、すくなくともWebアプリケーション側からファイルを参照・操作する場合はこの実装方法を推奨したい。