開発情報・ナレッジ

投稿者: ShiningStar株式会社 2025年3月25日 (火)

カスタムAPIでSPIRAL APIを効果的に扱うライブラリ

RESTful APIを簡単に利用するためのカスタムAPIクライアントのライブラリを作成しました。
カスタムAPIクライアントを使用することで、バックエンドとフロントエンドの連携が容易になり、
データベース操作やメール配信ジョブの管理などの機能を簡単にできるだけでなく、
複雑なAPI呼び出しをシンプルなメソッド呼び出しに抽象化することで、
開発効率の向上とコードの可読性を高めることができます。

注意点

APIキーなどの機密情報は適切に管理してください。
エラーハンドリングを適切に行い、ユーザーに分かりやすいメッセージを表示してください。

実装の概要

今回のコードでは、以下の3つの主要コンポーネントで構成されています。

1. PHP APIクライアント(api.php):サーバーサイドでAPIリクエストを処理
2. カスタムAPIファイル(bridge.php):フロントエンドとバックエンドの橋渡し役
3. JavaScriptクライアント(api-client.js):フロントエンドでAPIを呼び出すためのクラス

ファイルの設置場所

今回のコード紹介するファイルは以下の場所に設置してください。

api.php: PHPモジュール
bridge.php: カスタムAPI
api-client.js: サイトファイル
api-example.js: ページのJavaScript
api-example.html: ページのBODY

実装手順

1. サーバーサイドのAPIクライアント(api.php)をカスタムモジュールに配置
2. フロントエンドとバックエンドの橋渡し役となるカスタムAPIファイル(bridge.php)を配置
3. フロントエンド用のJavaScriptクライアント(api-client.js)をサイトファイルに配置
4. サンプルページ(api-example.html)を作成してクライアントを動作させるJavaScript(api-example.js)をJavaScriptタブに配置

事前準備

PHP APIクライアント(api.php)
<?php

// 設定
define('API_URL', 'https://api.spiral-platform.com/v1/');
define('API_KEY', 'your_api_key_here');
define('DEBUG_MODE', true);
define('API_TIMEOUT', 30);
define('API_RETRY_COUNT', 3);
define('API_RETRY_INTERVAL', 2);
define('DEFAULT_APP_ID', '100');
define('DEFAULT_DB_ID', '1');

/**
 * APIクライアント
 * リクエスト処理とエンドポイント管理を統合
 */
class Api {
    private static $instance = null;
    private $lastError = '';
    private $lastResponse = null;
    private $headers = [];
    
    /**
     * コンストラクタ
     */
    private function __construct() {
        $this->headers = [
            'Content-Type: application/json',
            'Authorization:Bearer ' . API_KEY,
            'X-Spiral-Api-Version: 1.1'
        ];
    }
    
    /**
     * インスタンス取得
     */
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * GETリクエスト
     */
    public function get($endpoint, $params = []) {
        return $this->request('GET', $endpoint, null, $params);
    }
    
    /**
     * POSTリクエスト
     */
    public function post($endpoint, $data = []) {
        return $this->request('POST', $endpoint, $data);
    }
    
    /**
     * PUTリクエスト
     */
    public function put($endpoint, $data = []) {
        return $this->request('PUT', $endpoint, $data);
    }
    
    /**
     * PATCHリクエスト
     */
    public function patch($endpoint, $data = []) {
        return $this->request('PATCH', $endpoint, $data);
    }
    
    /**
     * DELETEリクエスト
     */
    public function delete($endpoint, $params = []) {
        return $this->request('DELETE', $endpoint, null, $params);
    }
    
    /**
     * リクエスト実行
     */
    private function request($method, $endpoint, $data = null, $params = []) {
        $url = API_URL . $endpoint;
        
        // クエリパラメータの追加
        if (!empty($params) && ($method === 'GET' || $method === 'DELETE')) {
            $url .= '?' . http_build_query($params);
        }
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, API_TIMEOUT);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        
        if ($data !== null && ($method === 'POST' || $method === 'PUT' || $method === 'PATCH')) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }
        
        $retryCount = 0;
        $maxRetries = API_RETRY_COUNT;
        
        while ($retryCount <= $maxRetries) {
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            
            if ($response !== false) {
                $responseData = json_decode($response, true);
                $this->lastResponse = [
                    'response' => [
                        'info' => ['httpCode' => $httpCode],
                        'data' => $responseData
                    ]
                ];
                
                if ($httpCode >= 200 && $httpCode < 300) {
                    curl_close($ch);
                    return $this->lastResponse;
                }
                
                $this->lastError = "HTTPエラー: {$httpCode}";
                $retryCount++;
                
                if ($retryCount <= $maxRetries && $httpCode >= 500) {
                    sleep(API_RETRY_INTERVAL);
                    continue;
                }
                
                break;
            }
            
            $this->lastError = curl_error($ch);
            $retryCount++;
            
            if ($retryCount <= $maxRetries) {
                sleep(API_RETRY_INTERVAL);
            } else {
                break;
            }
        }
        
        curl_close($ch);
        return $this->lastResponse;
    }
    
    /**
     * レスポンスデータ取得
     */
    public function getResponseData($response = null) {
        if ($response === null) {
            $response = $this->lastResponse;
        }
        
        if (!$response || !isset($response['response']['data'])) {
            return null;
        }
        
        return $response['response']['data'];
    }
    
    /**
     * エラー取得
     */
    public function getLastError() {
        return $this->lastError;
    }
    
    /**
     * データベース一覧取得
     */
    public function getDatabases($params = [], $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        return $this->get("/apps/{$appId}/dbs", $params);
    }
    
    /**
     * データベースレコード取得
     */
    public function getDatabaseRecords($params = [], $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->get("/apps/{$appId}/dbs/{$dbId}/records", $params);
    }
    
    /**
     * データベースレコード作成
     */
    public function createDatabaseRecord($data, $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->post("/apps/{$appId}/dbs/{$dbId}/records", $data);
    }
    
    /**
     * データベースレコード更新
     */
    public function updateDatabaseRecord($recordId, $data, $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->put("/apps/{$appId}/dbs/{$dbId}/records/{$recordId}", $data);
    }
    
    /**
     * データベースレコード削除
     */
    public function deleteDatabaseRecord($recordId, $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->delete("/apps/{$appId}/dbs/{$dbId}/records/{$recordId}");
    }
    
    /**
     * EXPRESSメール配信ジョブ一覧取得
     * @param array $params クエリパラメータ(ids, isTest, offset, limit)
     * @param string|null $dbId データベースID
     * @param string|null $appId アプリID
     * @return array レスポンス
     */
    public function getEmailJobs($params = [], $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->get("/apps/{$appId}/dbs/{$dbId}/emailJobs", $params);
    }
    
    /**
     * データベースのフィールド一覧取得
     * @param array $params クエリパラメータ
     * @param string|null $dbId データベースID
     * @param string|null $appId アプリID
     * @return array レスポンス
     */
    public function getDatabaseFields($params = [], $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->get("/apps/{$appId}/dbs/{$dbId}", $params);
    }
}
?>

            

PHPのAPIクライアントは、RESTful APIへのリクエストを簡単に行うためのクラスです。
主な機能として、GET/POST/PUT/DELETEメソッドのリクエスト処理、エラーハンドリング、
そして各種APIエンドポイントへのアクセスメソッドを提供しています。

カスタムAPIファイル(bridge.php)
<?php
require_once 'api.php';  //カスタムモジュールのディレクトリを指定

// APIインスタンス取得
$api = Api::getInstance();

// SPIRALオブジェクトからリクエストデータを取得
$requestBody = $SPIRAL->getCustomApiRequestBody();
$queryParams = $SPIRAL->getCustomApiQueryParameters();

// アクション取得
$action = $queryParams['action'][0] ?? '';

// パラメータ処理
$appId = isset($requestBody['appId']) && $requestBody['appId'] !== '' ? $requestBody['appId'] : null;
$dbId = isset($requestBody['dbId']) && $requestBody['dbId'] !== '' ? $requestBody['dbId'] : null;

// レスポンス初期化
$response = [
    'status' => 'success',
    'data' => [
        'success' => false,
        'data' => null,
        'error' => null
    ]
];

try {
    // アクションに応じた処理
    switch ($action) {
        case 'getDatabases':
            $params = $requestBody['params'] ?? [];
            $result = $api->getDatabases($params, $appId);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'appId' => $appId ?? DEFAULT_APP_ID,
                    'params' => $params
                ]
            ];
            break;
            
        case 'getDatabaseRecords':
            $params = $requestBody['params'] ?? [];
            $result = $api->getDatabaseRecords($params, $dbId, $appId);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'appId' => $appId ?? DEFAULT_APP_ID,
                    'dbId' => $dbId ?? DEFAULT_DB_ID,
                    'params' => $params
                ]
            ];
            break;
            
        case 'createDatabaseRecord':
            $recordData = $requestBody['recordData'] ?? [];
            $result = $api->createDatabaseRecord($recordData, $dbId, $appId);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'appId' => $appId ?? DEFAULT_APP_ID,
                    'dbId' => $dbId ?? DEFAULT_DB_ID,
                    'recordData' => $recordData
                ]
            ];
            break;
            
        case 'getEmailJobs':
            $searchData = $requestBody['searchData'] ?? [];
            $result = $api->getEmailJobs($searchData);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'searchData' => $searchData
                    ]
                ];
                break;
            
        case 'getDatabaseFields':
            $params = $requestBody['params'] ?? [];
            $result = $api->getDatabaseFields($params, $dbId, $appId);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'appId' => $appId ?? DEFAULT_APP_ID,
                    'dbId' => $dbId ?? DEFAULT_DB_ID,
                    'params' => $params
                ]
            ];
            break;
            
        default:
            throw new Exception('不明なアクション: ' . $action);
    }
} catch (Exception $e) {
    $response['data']['error'] = ['message' => $e->getMessage()];
}

// レスポンス設定
$SPIRAL->setCustomApiResponse($response);
?>

            

カスタムAPIファイルは、フロントエンドからのリクエストを受け取り、
適切なAPIメソッドを呼び出して結果を返す役割を持ちます。
POSTリクエストのパラメータを解析し、アクションに応じた処理を実行します。

require_once 'api.php'; //カスタムモジュールのディレクトリを指定
の部分には、
api.phpを設置したPHPモジュールのディレクトリを指定してください。

JavaScriptクライアント(api-client.js)
const DEFAULT_BRIDGE_PATH = '/_program/bridge';

class ApiClient {
  constructor(baseUrl = '/api') {
    this.baseUrl = baseUrl;
    this.defaultAppId = '';
    this.defaultDbId = '';
  }

  /**
   * デフォルトのアプリIDを設定
   * @param {string} appId - アプリID
   */
  setDefaultAppId(appId) {
    this.defaultAppId = appId;
  }

  /**
   * デフォルトのデータベースIDを設定
   * @param {string} dbId - データベースID
   */
  setDefaultDbId(dbId) {
    this.defaultDbId = dbId;
  }

  /**
   * PHPブリッジを使用してAPIリクエストを実行
   * @param {string} action - 実行するアクション
   * @param {Object} data - リクエストデータ
   * @returns {Promise<Object>} レスポンス
   */
  async request(action, data = {}) {
    const url = `${DEFAULT_BRIDGE_PATH}?action=${action}`;
    
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      });
      
      if (!response.ok) {
        throw new Error(`HTTPエラー: ${response.status}`);
      }
      
      const result = await response.json();
      
      if (result.status === 'success' && result.data) {
        return result.data;
      } else {
        throw new Error(result.data?.error?.message || '不正なレスポンス形式です');
      }
    } catch (error) {
      console.error('APIリクエストエラー:', error);
      return {
        success: false,
        error: {
          message: error.message || 'APIリクエストに失敗しました'
        }
      };
    }
  }

  /**
   * データベース一覧を取得
   * @param {Object} params - クエリパラメータ
   * @param {string} appId - アプリID(省略可)
   * @returns {Promise<Object>} レスポンス
   */
  async getDatabases(params = {}, appId = null) {
    return this.request('getDatabases', {
      appId: appId || this.defaultAppId,
      params
    });
  }

  /**
   * データベースレコード一覧を取得
   * @param {Object} params - クエリパラメータ
   * @param {string} dbId - データベースID(省略可)
   * @param {string} appId - アプリID(省略可)
   * @returns {Promise<Object>} レスポンス
   */
  async getDatabaseRecords(params = {}, dbId = null, appId = null) {
    return this.request('getDatabaseRecords', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      params
    });
  }

  /**
   * 新しいデータベースレコードを作成
   * @param {Object} recordData - レコードデータ
   * @param {string} dbId - データベースID(省略可)
   * @param {string} appId - アプリID(省略可)
   * @returns {Promise<Object>} レスポンス
   */
  async createDatabaseRecord(recordData, dbId = null, appId = null) {
    return this.request('createDatabaseRecord', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      recordData
    });
  }

  /**
   * EXPRESSメール配信ジョブ一覧を取得
   * @param {Object} params - クエリパラメータ(ids, isTest, offset, limit)
   * @param {string} dbId - データベースID(省略可)
   * @param {string} appId - アプリID(省略可)
   * @returns {Promise<Object>} レスポンス
   */
  async getEmailJobs(params = {}, dbId = null, appId = null) {
    return this.request('getEmailJobs', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      params
    });
  }

  /**
   * データベースのフィールドリストを取得
   * @param {Object} params - クエリパラメータ
   * @param {string} dbId - データベースID(省略可)
   * @param {string} appId - アプリID(省略可)
   * @returns {Promise<Object>} レスポンス
   */
  async getDatabaseFields(params = {}, dbId = null, appId = null) {
    return this.request('getDatabaseFields', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      params
    });
  }
}

            

JavaScriptクライアントは、ブラウザからAPIを簡単に呼び出すためのクラスです。
Fetch APIを使用して非同期リクエストを行い、Promiseベースで結果を返します。
各種APIエンドポイントに対応するメソッドを提供し、エラーハンドリングも行います。

const DEFAULT_BRIDGE_PATH = '/_program/bridge';
の部分には、
カスタムAPIファイル(bridge.php)を設置したディレクトリを指定してください。

サンプル実装(api-example.js:ページのJavaScript)

// APIクライアントのインスタンスを作成
let api;

// api-client.jsが読み込まれているか確認する関数
function checkApiClientLoaded() {
  if (typeof ApiClient === 'undefined') {
    console.error('ApiClientが読み込まれていません。api-client.jsが正しく読み込まれているか確認してください。');
    // 500ミリ秒後に再試行
    setTimeout(checkApiClientLoaded, 500);
    return false;
  }
  
  console.log('ApiClientが正常に読み込まれました');
  // ApiClientが利用可能になったらインスタンスを作成
  api = new ApiClient();
  
  // DOM要素の参照を取得し、イベントリスナーを設定
  initializeApp();
  return true;
}

// DOM要素の参照
let outputDiv;
let loadingIndicator;

/**
 * アプリケーションの初期化
 */
function initializeApp() {
  outputDiv = document.getElementById('output');
  loadingIndicator = document.getElementById('loading');
  
  // イベントリスナーを設定
  setupEventListeners();
}

/**
 * 結果を表示
 * @param {string} title - タイトル
 * @param {Object} data - 表示データ
 */
function displayResult(title, data) {
  const resultDiv = document.createElement('div');
  resultDiv.className = 'result-item';
  
  const titleElement = document.createElement('h3');
  titleElement.textContent = title;
  resultDiv.appendChild(titleElement);
  
  const contentElement = document.createElement('pre');
  contentElement.className = 'json-content';
  contentElement.textContent = JSON.stringify(data, null, 2);
  resultDiv.appendChild(contentElement);
  
  outputDiv.prepend(resultDiv);
}

/**
 * エラーを表示
 * @param {string} title - タイトル
 * @param {Object|string} error - エラー情報
 */
function displayError(title, error) {
  const errorDiv = document.createElement('div');
  errorDiv.className = 'result-item error';
  
  const titleElement = document.createElement('h3');
  titleElement.textContent = title;
  errorDiv.appendChild(titleElement);
  
  const contentElement = document.createElement('pre');
  contentElement.className = 'error-content';
  
  if (typeof error === 'string') {
    contentElement.textContent = error;
  } else {
    contentElement.textContent = JSON.stringify(error, null, 2);
  }
  
  errorDiv.appendChild(contentElement);
  outputDiv.prepend(errorDiv);
}

/**
 * ローディング表示の切り替え
 * @param {boolean} isLoading - ローディング中かどうか
 */
function toggleLoading(isLoading) {
  loadingIndicator.style.display = isLoading ? 'block' : 'none';
}

// ===== サンプル実行関数 =====

/**
 * データベース一覧取得のサンプル
 * @param {string} appId - アプリID
 */
async function getDatabasesExample(appId = '') {
  toggleLoading(true);
  
  try {
    const result = await api.getDatabases({}, appId);
    if (result.status === "success") {
      displayResult('データベース一覧取得', result.data.data.result);
    } else {
      displayError('データベース一覧取得エラー', result.error);
    }
  } catch (error) {
    displayError('データベース一覧取得例外', error);
  }
  
  toggleLoading(false);
}

/**
 * データベースレコード一覧取得のサンプル
 * @param {string} appId - アプリID
 * @param {string} dbId - データベースID
 */
async function getDatabaseRecordsExample(appId = '', dbId = '') {
  toggleLoading(true);
  
  try {
    const params = {
      limit: 10,
      offset: 0
    };
    
    const result = await api.getDatabaseRecords(params, dbId, appId);
    
    if (result.status === "success") {
      displayResult('データベースレコード一覧取得', result.data.data.result);
    } else {
      displayError('データベースレコード一覧取得エラー', result.error);
    }
  } catch (error) {
    displayError('データベースレコード一覧取得例外', error);
  }
  
  toggleLoading(false);
}

/**
 * 新規データベースレコード作成のサンプル
 * @param {string} appId - アプリID
 * @param {string} dbId - データベースID
 */
async function createDatabaseRecordExample(appId = '', dbId = '') {
  toggleLoading(true);
  
  try {
    const recordData = {
      email: `test${Math.floor(Math.random() * 1000000)}@example.com`,
      firstName: 'テスト',
      lastName: 'ユーザー',
    };
    
    const result = await api.createDatabaseRecord(recordData, dbId, appId);
    
    if (result.status === "success") {
      displayResult('データベースレコード作成', result.data.data.result);
    } else {
      displayError('データベースレコード作成エラー', result.error);
    }
  } catch (error) {
    displayError('データベースレコード作成例外', error);
  }
  
  toggleLoading(false);
}

/**
 * EXPRESSメール配信ジョブ一覧取得のサンプル
 * @param {string} appId - アプリID
 * @param {string} dbId - データベースID
 */
async function getEmailJobsExample(appId = '', dbId = '') {
  toggleLoading(true);
  
  try {
    const params = {
      limit: 20,
      offset: 0,
      isTest: false
    };
    
    const result = await api.getEmailJobs(params, dbId, appId);
    
    if (result.status === "success") {
      displayResult('メール配信ジョブ一覧取得', result.data.data.result);
    } else {
      displayError('メール配信ジョブ一覧取得エラー', result.error);
    }
  } catch (error) {
    displayError('メール配信ジョブ一覧取得例外', error);
  }
  
  toggleLoading(false);
}

/**
 * データベースのフィールドリスト取得のサンプル
 * @param {string} appId - アプリID
 * @param {string} dbId - データベースID
 */
async function getDatabaseFieldsExample(appId = '', dbId = '') {
  toggleLoading(true);
  
  try {
    const params = {};
    
    const result = await api.getDatabaseFields(params, dbId, appId);
    
    if (result.status === "success") {
      // レスポンスデータの構造を正しく処理
      let fields = [];
      
      // 入れ子になったデータ構造を適切に処理
      if (result.data && result.data.data && result.data.data.result && result.data.data.result.fields) {
        fields = result.data.data.result.fields;
        displayResult('データベースフィールド一覧取得', fields);
        
        // フィールドに基づいて動的フォームを生成
        generateDynamicForm(fields);
      } else {
        displayError('データベースフィールド一覧取得エラー', 'フィールドデータが見つかりません');
      }
    } else {
      displayError('データベースフィールド一覧取得エラー', result.error);
    }
  } catch (error) {
    displayError('データベースフィールド一覧取得例外', error);
  }
  
  toggleLoading(false);
}

/**
 * フィールドリストに基づいて動的フォームを生成
 * @param {Array} fields - フィールドリスト
 */
function generateDynamicForm(fields) {
  // 既存のフォームがあれば削除
  const existingForm = document.getElementById('dynamic-form-container');
  if (existingForm) {
    existingForm.remove();
  }
  
  // 新しいフォームコンテナを作成
  const formContainer = document.createElement('div');
  formContainer.id = 'dynamic-form-container';
  formContainer.className = 'result-item';
  
  const formTitle = document.createElement('h3');
  formTitle.textContent = 'レコード登録フォーム';
  formContainer.appendChild(formTitle);
  
  // フォーム要素を作成
  const form = document.createElement('form');
  form.id = 'record-form';
  
  // フィールドごとに入力要素を追加
  fields.forEach(field => {
    const fieldContainer = document.createElement('div');
    fieldContainer.className = 'form-field';
    
    const label = document.createElement('label');
    label.setAttribute('for', `field-${field.name}`);
    label.textContent = `${field.displayName || field.name}${field.required ? ' *' : ''}`;
    fieldContainer.appendChild(label);
    
    // フィールドタイプに応じた入力要素を作成
    let input;
    
    switch (field.type) {
      case 'select':
        // 通常のセレクトボックス
        input = document.createElement('select');
        
        // 空のオプションを追加
        const emptyOption = document.createElement('option');
        emptyOption.value = '';
        emptyOption.textContent = '選択してください';
        input.appendChild(emptyOption);
        
        // 選択肢を追加
        if (field.options && Array.isArray(field.options)) {
          field.options.forEach(option => {
            const optionElement = document.createElement('option');
            optionElement.value = option.value || option.id || '';
            optionElement.textContent = option.label || option.name || option.value || '';
            input.appendChild(optionElement);
          });
        }
        break;
        
      case 'multiselect':
        // マルチセレクトの場合はチェックボックスグループとして実装
        // チェックボックスグループのコンテナ
        const checkboxContainer = document.createElement('div');
        checkboxContainer.className = 'checkbox-group';
        
        // 選択肢を追加
        if (field.options && Array.isArray(field.options)) {
          field.options.forEach(option => {
            const checkboxWrapper = document.createElement('div');
            checkboxWrapper.className = 'checkbox-item';
            
            // チェックボックス作成
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = `field-${field.name}-${option.value || option.id || ''}`;
            checkbox.name = field.name;
            checkbox.value = option.value || option.id || '';
            checkbox.className = 'checkbox-input';
            
            // ラベル作成
            const checkboxLabel = document.createElement('label');
            checkboxLabel.className = 'checkbox-label';
            checkboxLabel.setAttribute('for', checkbox.id);
            checkboxLabel.textContent = option.label || option.name || option.value || '';
            
            // 追加
            checkboxWrapper.appendChild(checkboxLabel);
            checkboxWrapper.appendChild(checkbox);
            checkboxContainer.appendChild(checkboxWrapper);
          });
        }
        
        input = checkboxContainer;
        break;
        
      case 'textarea':
        input = document.createElement('textarea');
        input.rows = 4;
        break;
        
      case 'date':
        input = document.createElement('input');
        input.type = 'date';
        break;
        
      case 'datetime':
        input = document.createElement('input');
        input.type = 'datetime-local';
        break;
        
      case 'number':
        input = document.createElement('input');
        input.type = 'number';
        break;
        
      case 'email':
        input = document.createElement('input');
        input.type = 'email';
        break;
        
      case 'tel':
        input = document.createElement('input');
        input.type = 'tel';
        break;
        
      case 'password':
        input = document.createElement('input');
        input.type = 'password';
        break;
        
      default:
        input = document.createElement('input');
        input.type = 'text';
    }
    
    // 共通属性を設定
    input.id = `field-${field.name}`;
    if (field.type !== 'multiselect') {
      input.name = field.name;
    }
    input.className = 'form-input';
    
    if (field.required) {
      input.required = true;
    }
    
    if (field.placeholder) {
      input.placeholder = field.placeholder;
    }
    
    fieldContainer.appendChild(input);
    form.appendChild(fieldContainer);
  });
  
  // 送信ボタンを追加
  const submitButton = document.createElement('button');
  submitButton.type = 'submit';
  submitButton.textContent = 'レコードを登録';
  submitButton.className = 'submit-button';
  form.appendChild(submitButton);
  
  // フォーム送信イベントを設定
  form.addEventListener('submit', handleFormSubmit);
  
  formContainer.appendChild(form);
  outputDiv.prepend(formContainer);
}

/**
 * フォームの送信処理
 * @param {SubmitEvent} event - 送信イベント
 */
function handleFormSubmit(event) {
  event.preventDefault();
  
  const formData = {};
  const formElements = event.target.elements;
  
  // フォームデータの収集
  for (let i = 0; i < formElements.length; i++) {
    const element = formElements[i];
    
    // ボタン以外の要素を処理
    if (element.tagName !== 'BUTTON') {
      // チェックボックスの場合は選択された値のみを配列で収集
      if (element.type === 'checkbox') {
        if (element.checked) {
          // 既に同じ名前のプロパティが存在するか確認
          if (formData[element.name]) {
            // 既に配列の場合は値を追加
            if (Array.isArray(formData[element.name])) {
              formData[element.name].push(element.value);
            } else {
              // 既存の値と新しい値で配列を作成
              formData[element.name] = [formData[element.name], element.value];
            }
          } else {
            // 新しいプロパティとして値を設定
            formData[element.name] = element.value;
          }
        }
      } else {
        // その他の入力要素
        formData[element.name] = element.value;
      }
    }
  }
  
  console.log('送信データ:', formData);
  
  // APIリクエストの実行
  submitDynamicForm(formData);
}

/**
 * 動的フォームからのデータを送信
 * @param {Object} formData - フォームデータ
 */
async function submitDynamicForm(formData) {
  toggleLoading(true);
  
  try {
    const appId = document.getElementById('api-app-id').value;
    const dbId = document.getElementById('api-db-id').value;
    
    const result = await api.createDatabaseRecord(formData, dbId, appId);
    
    if (result.status === "success") {
      displayResult('動的フォームからのレコード登録', result.data.data.result);
      
      // レスポンスの構造を確認して適切なメッセージを表示
      if (result.data && result.data.data && result.data.data.result) {
        const apiResult = result.data.data.result;
        
        // エラーの場合
        if (apiResult.status && apiResult.status === 400) {
          let errorMessage = `登録エラー: ${apiResult.message}\n\n`;
          
          if (apiResult.errors && apiResult.errors.length > 0) {
            errorMessage += '詳細エラー:\n';
            apiResult.errors.forEach(error => {
              errorMessage += `・${error.location}: ${error.message}\n`;
            });
          }
          
          alert(errorMessage);
        } 
        // 成功の場合
        else if (apiResult.item) {
          alert(`レコードが正常に登録されました。\nレコードID: ${apiResult.item._id}`);
        }
        // その他の場合
        else {
          alert('処理は完了しましたが、予期しない応答形式です。詳細は実行結果を確認してください。');
        }
      } else {
        alert('処理は完了しましたが、応答データの形式が不明です。詳細は実行結果を確認してください。');
      }
    } else {
      displayError('レコード登録エラー', result.error);
      alert(`レコード登録に失敗しました: ${result.error?.message || '不明なエラー'}`);
    }
  } catch (error) {
    displayError('レコード登録例外', error);
    alert(`レコード登録中に例外が発生しました: ${error.message || '不明なエラー'}`);
  }
  
  toggleLoading(false);
}

/**
 * イベントリスナーを設定
 */
function setupEventListeners() {
  // 設定保存ボタン
  document.getElementById('btn-save-config').addEventListener('click', () => {
    const appId = document.getElementById('api-app-id').value.trim();
    const dbId = document.getElementById('api-db-id').value.trim();
    
    if (appId) {
      api.setDefaultAppId(appId);
    }
    
    if (dbId) {
      api.setDefaultDbId(dbId);
    }
    
    alert('API設定を保存しました');
  });
  
  // データベース一覧取得ボタン
  document.getElementById('btn-get-databases').addEventListener('click', () => {
    const appId = document.getElementById('api-app-id').value.trim();
    getDatabasesExample(appId);
  });
  
  // データベースレコード取得ボタン
  document.getElementById('btn-get-records').addEventListener('click', () => {
    const appId = document.getElementById('api-app-id').value.trim();
    const dbId = document.getElementById('api-db-id').value.trim();
    getDatabaseRecordsExample(appId, dbId);
  });
  
  // メール配信ジョブ一覧取得ボタン
  document.getElementById('btn-get-email-jobs').addEventListener('click', () => {
    const appId = document.getElementById('api-app-id').value.trim();
    const dbId = document.getElementById('api-db-id').value.trim();
    getEmailJobsExample(appId, dbId);
  });
  
  // フィールドリスト取得ボタン
  document.getElementById('btn-get-fields').addEventListener('click', () => {
    const appId = document.getElementById('api-app-id').value.trim();
    const dbId = document.getElementById('api-db-id').value.trim();
    getDatabaseFieldsExample(appId, dbId);
  });
}

// DOMが読み込まれたら実行
document.addEventListener('DOMContentLoaded', () => {
  // api-client.jsが読み込まれているか確認
  checkApiClientLoaded();
});

            

サンプル実装(api-example.html:ページのBODY)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>REST APIクライアント サンプル</title>
  <style>
body {
    font-family: 'Helvetica Neue', Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
    background-color: #f5f5f5;
  }
  
  h1, h2 {
    color: #2c3e50;
  }
  
  .container {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
  }
  
  .control-panel {
    flex: 1;
    min-width: 300px;
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  }
  
  .output-panel {
    flex: 2;
    min-width: 500px;
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    max-height: 800px;
    overflow-y: auto;
  }
  
  .button-group {
    margin-bottom: 15px;
    padding-bottom: 15px;
    border-bottom: 1px solid #eee;
  }
  
  button {
    background-color: #3498db;
    color: white;
    border: none;
    padding: 8px 15px;
    margin-right: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: background-color 0.3s;
  }
  
  button:hover {
    background-color: #2980b9;
  }
  
  input, select {
    padding: 8px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    width: 100%;
  }
  
  label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
  }
  
  .result-item {
    margin-bottom: 20px;
    padding: 15px;
    border-radius: 4px;
    background-color: #f9f9f9;
    border-left: 4px solid #3498db;
  }
  
  .result-item.error {
    border-left-color: #e74c3c;
    background-color: #fdf0ed;
  }
  
  .result-item h3 {
    margin-top: 0;
    color: #2c3e50;
  }
  
  pre {
    background-color: #f5f5f5;
    padding: 10px;
    border-radius: 4px;
    overflow-x: auto;
    font-size: 14px;
    line-height: 1.4;
  }
  
  .json-content {
    color: #333;
  }
  
  .error-content {
    color: #e74c3c;
  }
  
  #loading {
    display: none;
    text-align: center;
    margin: 20px 0;
  }
  
  .spinner {
    border: 4px solid rgba(0, 0, 0, 0.1);
    border-radius: 50%;
    border-top: 4px solid #3498db;
    width: 30px;
    height: 30px;
    animation: spin 1s linear infinite;
    margin: 0 auto;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
  .checkbox-group {
    margin-bottom: 15px;
  }
  
  .checkbox-item {
    margin-bottom: 10px;
  }
  
  .checkbox-item label {
    margin-left: 5px;
  }
  
  /* マルチセレクト(チェックボックスグループ)のスタイル */
  .checkbox-group {
    display: flex;
    flex-direction: column;
    margin-bottom: 15px;
    padding: 5px;
    width: 100%;
  }
  
  .checkbox-item {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
    padding: 5px;
    width: 100%;
  }
  
  .checkbox-item:hover {
    background-color: #f5f5f5;
    border-radius: 4px;
  }
  
  .checkbox-input {
    margin-right: 10px;
    cursor: pointer;
    width:50%;
  }
  
  .checkbox-label {
    cursor: pointer;
    flex: 1;
  }
  </style>
</head>
<body>
  <h1>REST APIクライアント サンプル</h1>
  <p>このページでは、JavaScript APIクライアントの使用例を示します。各ボタンをクリックして、対応するAPIエンドポイントを呼び出すことができます。</p>
  
  <div class="container">
    <div class="control-panel">
      <h2>API設定</h2>
      <div>
        <label for="api-app-id">アプリID</label>
        <input type="text" id="api-app-id" placeholder="アプリID(省略可)">
      </div>
      <div>
        <label for="api-db-id">データベースID</label>
        <input type="text" id="api-db-id" placeholder="データベースID(省略可)">
      </div>
      <button id="btn-save-config">設定を保存</button>
      
      <h2>APIサンプル</h2>
      <div class="button-group">
        <h3>データベース操作</h3>
        <button id="btn-get-databases">データベース一覧取得</button>
        <button id="btn-get-records">データベースレコード取得</button>
        <button id="btn-get-fields">フィールドリスト取得</button>
      </div>
      
      <div class="button-group">
        <h3>メール配信</h3>
        <button id="btn-get-email-jobs">メール配信ジョブ一覧取得</button>
      </div>
    </div>
    
    <div class="output-panel">
      <h2>実行結果</h2>
      <div id="loading">
        <div class="spinner"></div>
        <p>処理中...</p>
      </div>
      <div id="output"></div>
    </div>
  </div>
  
  <script src="/_media/api-client.js"></script>
</body>
</html>
            

サンプル実装では、APIクライアントを使用して以下の操作を行う例を示しています:
- データベース一覧の取得
- データベースレコードの取得
- データベースレコードの作成
- メール配信ジョブ一覧の取得

HTMLファイル内のJavaScriptコードはサイトファイルに設置して読み込んでください。

主要な機能

データベース操作: データベースの一覧取得、レコードの作成・取得・更新・削除
メール配信管理: メール配信ジョブの一覧取得
エラーハンドリング: APIリクエスト時のエラー処理と適切なメッセージ表示
非同期処理: Promiseベースの非同期リクエスト処理

実装のポイント

1. 抽象化: 複雑なAPI呼び出しをシンプルなメソッド呼び出しに抽象化
2. エラーハンドリング: リクエスト失敗時の適切なエラー処理
3. 統一インターフェース: バックエンドとフロントエンドで一貫したインターフェース設計
4. 拡張性: 新しいAPIエンドポイントを簡単に追加できる設計

EXPRESSメール配信ジョブ一覧取得の実装例

例でEXPRESSメール配信ジョブ一覧を取得する機能を追加しました。
この機能は、指定したアプリIDとデータベースIDに関連するメール配信ジョブの一覧を取得します。

PHP側の実装:
public function getEmailJobs($params = [], $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->get("/apps/{$appId}/dbs/{$dbId}/emailJobs", $params);
    }
                

JavaScript側の実装:
  async getEmailJobs(params = {}, dbId = null, appId = null) {
    return this.request('getEmailJobs', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      params
    });
  }
                

使用例

以下のコードで、メール配信ジョブ一覧を取得できます:

// APIクライアントのインスタンスを作成
const api = new ApiClient();

// メール配信ジョブ一覧を取得
async function getEmailJobsExample(appId = '', dbId = '') {
  toggleLoading(true);
  
  try {
    const params = {
      limit: 20,
      offset: 0,
      isTest: false
    };
    
    const result = await api.getEmailJobs(params, dbId, appId);
    
    if (result.status === "success") {
      displayResult('メール配信ジョブ一覧取得', result.data.data.result);
    } else {
      displayError('メール配信ジョブ一覧取得エラー', result.error);
    }
  } catch (error) {
    displayError('メール配信ジョブ一覧取得例外', error);
  }
  
  toggleLoading(false);
}
            

APIの追加の仕方

新しいAPI機能を追加する場合、以下の3つのファイルを修正する必要があります。
ここでは例として、「getEmailJobs」APIを追加する手順を紹介します。

1. PHP APIクライアント(api.php)に新しいメソッドを追加
/**
 * メール配信ジョブ一覧取得
 */
public function getEmailJobs($params = [], $dbId = null, $appId = null) {
        $appId = ($appId === null || $appId === '') ? DEFAULT_APP_ID : $appId;
        $dbId = $dbId ?? DEFAULT_DB_ID;
        return $this->get("/apps/{$appId}/dbs/{$dbId}/emailJobs", $params);
    }
                
2. JavaScriptクライアント(api-client.js)に対応するメソッドを追加
/**
 * メール配信ジョブ一覧を取得
 * @param {Object} params - クエリパラメータ(ids, isTest, offset, limit)
 * @param {string} dbId - データベースID(省略可)
 * @param {string} appId - アプリID(省略可)
 * @returns {Promise<Object>} レスポンス
 */
  async getEmailJobs(params = {}, dbId = null, appId = null) {
    return this.request('getEmailJobs', {
      appId: appId || this.defaultAppId,
      dbId: dbId || this.defaultDbId,
      params
    });
  }
                
3. カスタムAPIファイル(bridge.php)にアクションケースを追加
case 'getEmailJobs':
            $searchData = $requestBody['searchData'] ?? [];
            $result = $api->getEmailJobs($searchData);
            
            $response['data']['success'] = true;
            $response['data']['data'] = [
                'result' => $api->getResponseData($result),
                'requestInfo' => [
                    'action' => $action,
                    'searchData' => $searchData
                    ]
                ];
                break;
                

APIを追加する際のポイント

1. エンドポイントの設計: RESTful APIの設計原則に従い、適切なエンドポイントパスを設定する
2. パラメータの標準化: appId、dbIdなどの共通パラメータは一貫した扱い方をする
3. エラーハンドリング: 適切なエラー処理を実装し、クライアントに分かりやすいメッセージを返す
4. ドキュメント化: 新しいAPIメソッドには適切なコメントを付け、使用方法を明確にする

まとめ

本記事では、RESTful APIを簡単に利用するためのカスタムAPIクライアントの実装方法について紹介しました。
PHPとJavaScriptを組み合わせることで、バックエンドとフロントエンドの連携を効率化し、
データベース操作やメール配信ジョブの管理などの機能を簡単に実装できます。
このAPIクライアントをベースに、さらに機能を拡張することで、より高度なアプリケーション開発が可能になります。
解決しない場合はこちら コンテンツに関しての
要望はこちら