FileMaker Data API を使う 2025年版 vol.8

 サンプルデータの作成は [FileMaker Data API を使う 2025 年版 vol.1](https://qiita.com/RemixWorksLLC/items/0e5f2741e7f50547b427) を参照してください。

 ## 利用環境

 以下の環境を前提に説明しています。

 - Claris FileMaker Pro 21.1.1.41 macOS
 - Claris FileMaker Server 21.1.1.40 Ubuntu 22(AMD)
 - サーバ: Ubuntu 22.04.5 LTS
 - SSL 証明書
 - リクエストする側のサーバ（任意）

 開発段階では、以下を使用した方が楽でしょう。

 - [Postman](https://www.postman.com)

 必要なクラスファイルは、[filemaker-data-api-v2.2.php](https://qiita.com/RemixWorksLLC/items/829f963fea79ac53abe4#完成版-filemaker-data-api-v22php)

テンプレートファイルは、[login_log_out.php](https://qiita.com/RemixWorksLLC/items/a0b787fce6a97a8ccd72#login-log_outphp-テンプレート)

## Execute Script

 FileMaker Data API を使って、スクリプトを実行できます。リクエストメソッドは `GET` で、渡せるクエリパラメータはスクリプトに渡す引数のみです。

 まず、`filemaker-data-api-v.2.2.php` に `executeScript` メソッドを追加して、ファイル名を `filemaker-data-api-v2.3.php` に変更します。

 ### executeScript() メソッド
```
	// スクリプトの実行
	public function executeScript(
		string $database,
		string $layout,
		string $bearer_session_token,
		string $script_name,
		string $script_parameters = ''
		): string
	{
		$authorization      = 'Authorization: Bearer ' . $bearer_session_token;
		$curlopt_httpheader = array($authorization);
		$script_parameters	= !empty($script_parameters) ? '?script.param=' . json_encode(json_decode($script_parameters)) : '';
		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/layouts/{$layout}/script/{$script_name}{$script_parameters}";
		
		return self::executeCurl($endpoint, 'GET', $curlopt_httpheader);
	}
    
```

 次に、実行するスクリプトを FileMaker Pro で作成します。
 強引な気もしますが、内容は Get Records、Find Records でやった2024年出塁率ランキングベスト10 を FileMaker スクリプトで作成し、それを戻り値として返します。
 FileMaker Data API のスクリプトの戻り値は、実行するスクリプトごとに纏まって返ってくるので、今回のように値が複数の場合、少々、面倒です。

### obp_ranking
```
変数を設定 [ $json ; 値: Get ( スクリプト引数 ) ]
# 
変数を設定 [ $year ; 値: JSONGetElement ( $json ; "year" ) ]
変数を設定 [ $offset ; 値: JSONGetElement ( $json ; "offset" ) - 1 ]
変数を設定 [ $limit ; 値: JSONGetElement ( $json ; "limit" ) ]
# 
変数を設定 [ $query ; 値: "SELECT \"last_name_ first_name\", on_base_percent FROM player_batting WHERE 	\"year\" = " & "'" & $year & "'" & ¶ & "ORDER BY on_base_percent DESC¶" & "OFFSET " & $offset  & " ROWS¶" & "FETCH FIRST " & $limit & " ROWS ONLY" ]
# 
変数を設定 [ $result ; 値: ExecuteSQLe ( $query ; ":" ; "" ) ]
# 
変数を設定 [ $count ; 値: 1 ]
Loop [ フラッシュ: 常に ]
	Exit Loop If [ $count > ValueCount ( $result ) ]
	変数を設定 [ $temp ; 値: Substitute ( GetValue ( $result ; $count ) ; ":" ; "¶" ) ]
	変数を設定 [ $player ; 値: GetValue ( $temp ; 1 ) ]
	変数を設定 [ $obp ; 値: GetValue ( $temp ; 2 ) ]
	変数を設定 [ $response ; 値: $response & JSONSetElement ( 	"[]" ; 	[ "[+].player" ; $player ; JSONString ] ; 	[ "[:].obp" ; $obp ; JSONString ] ) & "," ]
	変数を設定 [ $count ; 値: $count + 1 ]
End Loop
変数を設定 [ $response ; 値: Left ( $response ; Length ( $response ) - 1 ) ]
# 
現在のスクリプト終了 [ テキスト結果: $response ]

```

　JSON で、キー、`year`、`offset`、`limit` を渡してもらい、これを使って SQL クエリを作成します。
 FileMaker Data API の `offset` は 1から数えますが、SQL クエリは、0から数えますので、コーディング上、FileMaker Data API に合わせて、FileMaker スクリプトで、`- 1` しています。

  FileMaker の SQL クエリは、`ExecuteSQL()`関数、または `ExecuteSQLe()`関数を使います。構文はどちらも一緒ですが、`ExecuteSQLe()`関数の場合、エラーの際の表示が多少、細かくなります。

  ExecuteSQL() の SQL クエリでは、ダイナミック引数というプレースホルダが使えますが、これは、SQL の句によって効かない場合があります。トラブルを避けるため、ダイナミック引数でエラーが出る可能性のある SQL クエリは、ダイナミック引数を使わないで、SQL を 文字列として作ってしまった方がいいでしょう。
　使っている SQL クエリを整型して掲載しておきます。
```
SELECT
    "last_name_ first_name", on_base_percent
FROM 
    player_batting
WHERE
    "year" = '$year'
ORDER BY
    on_base_percent DESC
OFFSET $offset ROWS
FETCH FIRST $limit ROWS ONLY

```

　これをダブルクォーテーションで囲って文字列にするわけですから、クエリの中のダブルクォーテーションはエスケープする必要があります。

 結果セットの区切り文字は、カンマではなく、`:` コロンを採用しています。
 これは、STATCAST のデータの`last_name first_name` において、姓と名がカンマで区切られているためです。
 　そしてループ内で、今度はコロンを改行に変換し、Value List（改行区切りリスト）に変換し、JSON 風の文字列にして、戻り値としています。

 メインプログラムは、次の通りです。

 ### execute_script.php
```
<?php

require_once(__DIR__ . '/filemaker-data-api-v2.3.php');

$host                   = '（FileMaker Server のドメイン名）';
$version                = 'vLatest';
$database               = '（FileMaker ソリューション名）';
$username               = '（FileMaker ユーザ名）';
$password               = '（FileMaker パスワード）';
$layout                 = 'player_batting_basic';
$bearer_session_token   = null;

$fmda   = new FileMaker_Data_API($host, $version);

// 次のメタデータ取得のリクエストはログインする必要がない。
// Product Info
// Database Names

/**
 * Login begin
 */
// ログイン
$response = $fmda->login($database, $username, $password);

// ログイン成功？
if ($fmda->getMessagesCode($response) === '0') {
    // ログイン成功なら、Bearer Session Token を取得する
    $bearer_session_token   = $fmda->getBearerSessionToken($response);
} else {
    // ログイン失敗の処理を書く
    echo "FileMaker Data API へのログインに失敗しました。\n";
}
/**
 * Login end
 */

// （何らかの処理があり、リンクでページ遷移した想定）

// セッションの検証
$response   = $fmda->validateSession($bearer_session_token);
if ($fmda->getMessagesCode($response) === '0') {
    // セッション有効なら、以下の任意のリクエストを実行
    $script_name    = 'obp_ranking';
	$script_parameters = <<<_JSON_
{
    "year" : "2024",
    "offset" : 1,
    "limit" : 10
}
_JSON_;

    // スクリプト実行
    $response = $fmda->executeScript($database, $layout, $bearer_session_token, $script_name, $script_parameters);

    // 結果セット処理
    $result = $fmda->json2array($response);
    
    $player_obp = $result['response']['scriptResult'];
    $player_obp = '{"fieldData":[' . str_replace(['[', ']'] , '', $player_obp) . ']}';
    $player_obp = json_decode($player_obp);

    foreach ($player_obp->fieldData as $key => $value) {
        echo $value->player . ' - ';
        echo $value->obp, PHP_EOL;
    }

} else {
    // セッション無効なら、必要な Web データを保持して、ユーザに再ログインを促す
    echo "再ログインしてください。\n";
}

// レコード取得

// （何らかの処理）

/**
 * Log out begin
 */
// ログアウト

$response = $fmda->logOut($database, $bearer_session_token);
/**
 * Log out end
 */
```

　JSON で引数を作り、レスポンスは `stdClass` のプロパティとして表示させています。
 結果は次のようになります。
```
Judge, Aaron - .458
Soto, Juan - .419
Guerrero Jr., Vladimir - .396
Alvarez, Yordan - .392
Ohtani, Shohei - .390
Witt Jr., Bobby - .389
Profar, Jurickson - .380
Freeman, Freddie - .378
Ozuna, Marcell - .378
Harper, Bryce - .373
```

 今回のようにスクリプトの戻り値に結果セットを入れ込んでしまうのは、やはりイレギュラーなやり方だと思います。スクリプトの戻り値は、エラーコードのようなものや、単純な文字列の方が、コーディングがシンプルになって、開発時間も短くなると思います。
 もっとスマートな方法、この場合でしたら、`Find Reecords` を使った方が素早く出来上がると思います。
　FileMaker スクリプトは、柔軟にその場でどんどん作っていけるのですが、ルールが緩いだけに、規模が大きくなると、全体像が見えにくくなりますので、注意しましょう。

 　パフォーマンスとの兼ね合いになりますが、複雑なことは、力技でスクリプトやカスタム関数を使わないで、JavaScript を使用して、結果をもらった方がスッキリいく場合も少なからずあると思います。

### 完成版 filemaker-data-api-v2.3.php
```
<?php

class FileMaker_Data_API
{
    public $host, $version, $database, $username, $password, $layout;
    public $bearer_session_token;

    function __construct (string $host, string $version)
    {
        $this->host     = $host;
        $this->version  = $version;
    }

    /**
     * リクエスト
     **/

    // ログイン
    public function login (string $database, string $username, string $password): string
    {
		$this->database	= $database;
		$this->username = $username;
		$this->password = $password;

		$auth_value         = $username . ':' . $password;
		$authorization      = 'Authorization: Basic ' . base64_encode($auth_value);
		$content_type       = 'Content-Type: application/json';
		$curlopt_httpheader = array($authorization, $content_type);

		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/sessions";
		
		return self::executeCurl($endpoint, 'POST', $curlopt_httpheader);
	}

    // ログアウト
    public function logOut(string $database, string $bearer_session_token): string
    {
		$endpoint    = "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/sessions/{$bearer_session_token}";

		return self::executeCurl($endpoint, 'DELETE');
	}

    // データベースセッションの検証
	public function validateSession (string $session_token): string
    {
		$authorization      = 'Authorization: Bearer ' . $session_token;
		$curlopt_httpheader = array($authorization);
		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/validateSession";

		return self::executeCurl($endpoint, 'GET', $curlopt_httpheader);
	}

	// レコード範囲の取得
	public function getRecords(
		string $database,
		string $layout,
		string $bearer_session_token,
		array &$script_name_array,
		array &$script_param_array,
		int $_offset = 1,
		int $_limit = 100,
		string $_sort = ''
		): string
    {
		$this->layout		= $layout;

		$authorization      = 'Authorization: Bearer ' . $bearer_session_token;
		$curlopt_httpheader = array($authorization);

		// ソートの設定
		$_sort_string		= !empty($_sort) ? '&_sort=' . urlencode($_sort) : '';

        $script_prerequest			= '';
    	$script_prerequest_param	= '';
    	$script_presort				= '';
    	$script_presort_param		= '';
    	$script						= '';
    	$script_param				= '';
		// リクエスト処理前のスクリプト
		if ($script_name_array[0] !== '') {
			$script_prerequest	= '&script.prerequest=' . $script_name_array[0];
			$script_prerequest_param = '&script.prerequest.param=' . urlencode(json_encode($script_param_array[0]));
		}
		
		// ソート前のスクリプト
		if ($script_name_array[1] !== '') {
			$script_presort	= '&script.presort=' . $script_name_array[1];
			$script_presort_param = '&script.presort.param=' . urlencode(json_encode($script_param_array[1]));
		}

		// リクエスト処理後のスクリプト
		if ($script_name_array[2] !== '') {
			$script	= '&script=' . $script_name_array[2];
			$script_param = '&script.param=' . urlencode(json_encode($script_param_array[2]));
		}
		
		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/layouts/{$layout}/records?_offset={$_offset}&_limit={$_limit}{$_sort_string}{$script_prerequest}{$script_prerequest_param}{$script_presort}{$script_presort_param}{$script}{$script_param}";
		return self::executeCurl($endpoint, 'GET', $curlopt_httpheader);
	}

	// 検索の実行
	public function findRecords(
		string $database,
		string $layout,
		string $bearer_session_token,
		string $request_body
		): string
	{
		$authorization      = 'Authorization: Bearer ' . $bearer_session_token;
		$content_type       = 'Content-Type: application/json';
		$curlopt_httpheader = array($authorization, $content_type);
		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/layouts/{$layout}/_find";

		return self::executeCurl($endpoint, 'POST', $curlopt_httpheader, $request_body);
	}

	// スクリプトの実行
	public function executeScript(
		string $database,
		string $layout,
		string $bearer_session_token,
		string $script_name,
		string $script_parameters = ''
		): string
	{
		$authorization      = 'Authorization: Bearer ' . $bearer_session_token;
		$curlopt_httpheader = array($authorization);
		$script_parameters	= !empty($script_parameters) ? '?script.param=' . json_encode(json_decode($script_parameters)) : '';
		$endpoint        	= "https://{$this->host}/fmi/data/{$this->version}/databases/{$database}/layouts/{$layout}/script/{$script_name}{$script_parameters}";
		
		return self::executeCurl($endpoint, 'GET', $curlopt_httpheader);
	}

    /**
     * ユーティリティ
     **/

    // レスポンスコードの取得
	public function getMessagesCode (string $response): string
    {
		$response_array = self::json2array($response);
		return $response_array['messages'][0]['code'];
	}

    // Bearer Session Token 取得
    public function getBearerSessionToken (string $response): string
    {
		$response_array = self::json2array($response);
		$this->bearer_session_token = $response_array['response']['token'];

		return $this->bearer_session_token;
	}

    // JSON を 配列へ変換
	public function json2array (string $json): array
    {
		$json   = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
		return json_decode($json, true);
	}

    // cURL 実行
    static function executeCurl(string $curlopt_url, string $curlopt_customrequest, array $curlopt_httpheader = [], string $current_postfields = ''): string
    {
		$current_postfields	= !empty($current_postfields) ? json_encode(json_decode($current_postfields)) : '{}';

		$curl   = curl_init();
	
		curl_setopt_array($curl, array(
			CURLOPT_URL => $curlopt_url,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_ENCODING => '',
			CURLOPT_MAXREDIRS => 10,
			CURLOPT_TIMEOUT => 0,
			CURLOPT_FOLLOWLOCATION => true,
			CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
			CURLOPT_CUSTOMREQUEST => $curlopt_customrequest,
			CURLOPT_POSTFIELDS => $current_postfields,
			CURLOPT_HTTPHEADER => $curlopt_httpheader,
		));
	
		$response   = curl_exec($curl);
	
		curl_close($curl);
		return $response;
	}
}
```