開発情報・ナレッジ

投稿者: ShiningStar株式会社 2026年4月24日 (金)

フォームに金融機関・支店の検索機能を組み込むサンプルプログラム(BankcodeJP連携)

入力フォーム上で金融機関・支店を検索一覧から選べるようにし、選択結果をフォーム項目へ格納する構成を紹介します。
検索用の中継PHPは、本フォームとは別にSPIRAL内にもう1つ「ダミーフォーム」(見えないダミー項目だけで構成した空フォーム)を作成し、
そのフォームのHTMLソース編集画面に貼り付ける前提です。ダミーフォームのURLがそのままブラウザから叩ける中継エンドポイントになります。

注意点

APIキーは
PHPコード
(サーバ側)に保持し、
JSコード
(クライアント)には置きません。
中継用ダミーフォームは、入力項目や確認画面を持たない・メール配信もしない設定にしておき、PHPを貼り付けるためだけの空フォームとして運用してください。
無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
curl
のタイムアウト(接続・全体)は短めに設定し、外部API遅延で画面が固まらないようにしてください。
スパイラルPHPでは
header()
が使えないため、ステータス相当の値はJSON本文の
status
ok
フィールドに入れて返します。

実装の概要

事前準備. BankcodeJP で APIキー取得。SPIRAL内に「中継用ダミーフォーム」を1つ作成し、そのHTMLソースに
PHPコード
を貼り付けます。
JSコード
は本来の入力フォーム(銀行を選ばせたい画面)のHTMLソースに貼り付けます。
1. 入力フォーム側の
JSコード
が、ユーザー入力に応じて中継用ダミーフォームのURLへ
fetch
する。
2. 中継フォームに貼られた
PHP
が入力を検証し、
curl
で BankcodeJP を呼び、結果JSONを
<script>
タグで包んで出力する。
3.
JSコード
DOMParser
で該当タグを取り出し、
JSON.parse
して候補一覧を表示する(金融機関確定後に支店検索)。
4. ユーザーが候補を選んだら、金融機関コード・名称、支店コード・名称を hidden 等に格納し、本来の入力フォームを送信する。

事前準備

以下を用意します。識別名は環境に合わせて読み替えてください。

用途識別名(例)フィールドタイプ
金融機関コード(4桁・数字のみ)
bank_code
数字・記号・アルファベット(6バイト)
金融機関名
bank_name
テキストフィールド(128バイト)
支店コード(3桁・数字のみ)
branch_code
数字・記号・アルファベット(6バイト)
支店名
branch_name
テキストフィールド(128バイト)
フィルタ構文の詳細は BankcodeJP API Documentation
filter
を参照してください(例: ひらがな前方一致
hiragana==あ*
)。

設定方法

1. 中継用ダミーフォームに貼り付けるPHP(curl で BankcodeJP を呼ぶ中継)

SPIRAL内に中継用のダミーフォームを作成し、その フォーム > ソースデザイン に以下を貼り付けます。

<?//<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=OFF NAME=bankcodejp-proxy -->?>
<?php
/**
 * SPIRAL ver.1 で、検索用の中継PHPとして貼り付けるコード。
 *
 * 【どこに貼るか】
 * SPIRAL内に「ダミーフォーム」(見えないダミー項目のみで構成した空のフォーム)を作成し、
 * そのフォームの「ソースデザイン」画面に本PHPをそのまま貼り付ける。
 * フォームのURLが、そのままブラウザから叩ける中継エンドポイントになる。
 *
 * 【この構成のポイント】
 * - BankcodeJP への HTTPS 取得は curl 系で行う(curl_init / curl_setopt / curl_exec / curl_close は ver.1 の禁止一覧に無いため利用可)。
 * - file_get_contents / fopen / stream_* / header / http_response_code は禁止一覧のため使用しない。
 * - APIキーはこのサーバ側PHPに保持するため、クライアントには露出しない。
 *
 */

$BANKCODEJP_API_KEY  = '***REPLACE_WITH_YOUR_KEY***';
$BANKCODEJP_API_BASE = 'https://apis.bankcode-jp.com/v3';

$response = null;

try {
    $mode = (string)($_GET['mode'] ?? '');

    // q はJS側で UTF-8 → base64 にエンコードして渡してもらう。
    // SPIRAL ver.1 のフォーム文字コードが UTF-8 でない(Shift_JIS / EUC-JP 等)場合でも
    // base64 は ASCII のみなので影響を受けない。base64_decode の結果は UTF-8 バイト列になる。
    $qRaw = (string)($_GET['q'] ?? '');
    $q    = $qRaw === '' ? '' : (string)base64_decode($qRaw, true);
    $q    = trim($q);
    $bank = preg_replace('/\D/', '', (string)($_GET['bank'] ?? ''));

    if ($q === '' || mb_strlen($q) > 32) {
        throw new RuntimeException('invalid q');
    }
    $safe = preg_replace('/[^\x{3041}-\x{3096}A-Za-z0-9\-]/u', '', $q);
    if ($safe === null || $safe === '') {
        throw new RuntimeException('invalid characters');
    }

    $filter = 'hiragana==' . $safe . '*';
    $query  = http_build_query([
        'apikey' => $BANKCODEJP_API_KEY,
        'limit'  => 30,
        'filter' => $filter,
        'fields' => 'code,name,hiragana',
    ]);

    if ($mode === 'bank') {
        $url = $BANKCODEJP_API_BASE . '/banks?' . $query;
    } elseif ($mode === 'branch' && strlen($bank) === 4) {
        $url = $BANKCODEJP_API_BASE . '/banks/' . $bank . '/branches?' . $query;
    } else {
        throw new RuntimeException('invalid mode or bank');
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
    $body     = curl_exec($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException('upstream unreachable: ' . $curlErr);
    }
    if ($httpCode >= 400) {
        throw new RuntimeException('upstream status ' . $httpCode);
    }

    $decoded = json_decode((string)$body, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('invalid upstream json');
    }

    $response = ['ok' => true, 'status' => 200, 'data' => $decoded];
} catch (Throwable $e) {
    $response = ['ok' => false, 'status' => 400, 'error' => $e->getMessage()];
}

// SPIRALがHTMLテンプレートに埋め込み、<script>タグ除去や < → &lt; エスケープをかけても
// base64 は A-Z a-z 0-9 + / = のみで構成されるため壊れない。
// 一意なテキストマーカーで base64 文字列を挟んで出力し、JS 側で substring 抽出 → atob → JSON.parse する。
$json   = json_encode($response, JSON_UNESCAPED_UNICODE);
$b64    = base64_encode($json);
echo "\n__BANKCODEJP_RESULT_START__" . $b64 . "__BANKCODEJP_RESULT_END__\n";

2. 入力フォーム側に貼り付けるJS(fetch → HTMLパース → JSON取り出し)
銀行を選ばせたい本来の入力フォームの フォーム > ソースデザイン に以下のJSを貼り付けます。
fetch
の結果を
response.text()
で受け、
DOMParser
#bankcodejp-result
を取り出して
JSON.parse
します。
BANK_PROXY_URL
は中継用ダミーフォームのURLに差し替えてください。
<!--
  SPIRAL ver.1 の入力フォーム(銀行を選ばせたい画面)のソースデザインに貼り付けるブロック。
  hidden input として bank_code / bank_name / branch_code / branch_name を定義し、
  検索で選んだ値をそのまま hidden に書き込んで送信する。

  dlタグをformタグ内に配置する。
-->

<input type="hidden" name="bank_code"   id="f-bank-code">
<input type="hidden" name="bank_name"   id="f-bank-name">
<input type="hidden" name="branch_code" id="f-branch-code">
<input type="hidden" name="branch_name" id="f-branch-name">

<dl class="cf">
  <dt class="title">金融機関検索<span class="caution">ひらがな前方一致</span></dt>
  <dd class="data">
    <input class="input" type="text" id="bank-q" placeholder="例: みずほ" maxlength="32">
    <button type="button" id="bank-search-btn">検索</button>
    <ul id="bank-list"></ul>
    <p id="bank-selected" style="display:none;">
      選択中: <strong id="bank-selected-name"></strong>(<span id="bank-selected-code"></span>)
      <button type="button" id="bank-reset-btn">変更</button>
    </p>
    <span class="msg" id="bank-search-msg"></span>
  </dd>
</dl>

<dl class="cf" id="branch-search-area" style="display:none;">
  <dt class="title">支店検索<span class="caution">ひらがな前方一致</span></dt>
  <dd class="data">
    <input class="input" type="text" id="branch-q" placeholder="例: しんじゅく" maxlength="32">
    <button type="button" id="branch-search-btn">検索</button>
    <ul id="branch-list"></ul>
    <p id="branch-selected" style="display:none;">
      選択中: <strong id="branch-selected-name"></strong>(<span id="branch-selected-code"></span>)
    </p>
  </dd>
</dl>

<script>
// PHPを貼り付けた中継用ダミーフォームのフルURL(環境に合わせて差し替える)
const BANK_PROXY_URL = 'https://area18.smp.ne.jp/area/is?SMPFORM=pbsj-xxxx-yyyyyyyyyyyy';

// ---- base64(UTF-8対応)ユーティリティ ----
function b64EncodeUtf8(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  return btoa(bin);
}
function b64DecodeUtf8(b64) {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return new TextDecoder('utf-8').decode(bytes);
}

async function callBankProxy(params) {
  const sep = BANK_PROXY_URL.indexOf('?') >= 0 ? '&' : '?';
  const sendParams = Object.assign({}, params);
  if (typeof sendParams.q === 'string') sendParams.q = b64EncodeUtf8(sendParams.q);
  const url = BANK_PROXY_URL + sep + new URLSearchParams(sendParams).toString();
  const res = await fetch(url, { credentials: 'same-origin' });
  if (!res.ok) throw new Error('proxy http ' + res.status);

  const html = await res.text();
  const START = '__BANKCODEJP_RESULT_START__';
  const END   = '__BANKCODEJP_RESULT_END__';
  const s = html.indexOf(START);
  const e = html.indexOf(END, s >= 0 ? s : 0);
  if (s < 0 || e < 0) {
    console.log('[bankcodejp] raw response:', html);
    throw new Error('result marker not found');
  }
  const b64 = html.substring(s + START.length, e).trim();

  let payload;
  try {
    payload = JSON.parse(b64DecodeUtf8(b64));
  } catch (err) {
    throw new Error('invalid base64/json between markers');
  }
  if (!payload.ok) throw new Error(payload.error || 'proxy error');
  return payload.data;
}

async function fetchBanks(hiraganaPrefix) {
  const safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
  if (!safe) throw new Error('invalid input');
  return callBankProxy({ mode: 'bank', q: safe });
}

async function fetchBranches(bankCode4, hiraganaPrefix) {
  const safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
  if (!safe) throw new Error('invalid input');
  if (!/^\d{4}$/.test(bankCode4)) throw new Error('invalid bank code');
  return callBankProxy({ mode: 'branch', q: safe, bank: bankCode4 });
}

// ---- UI 配線 ----
(function () {
  const $ = (id) => document.getElementById(id);
  const msg = (t) => { $('bank-search-msg').textContent = t || ''; };

  // 本ブロック先頭で定義した hidden input
  const bankCodeInput   = $('f-bank-code');
  const bankNameInput   = $('f-bank-name');
  const branchCodeInput = $('f-branch-code');
  const branchNameInput = $('f-branch-name');

  // 選択状態は JS 内部で保持
  let selectedBankCode = '';
  let selectedBankName = '';

  function renderList(listEl, items, onPick) {
    listEl.innerHTML = '';
    if (!items || items.length === 0) {
      listEl.innerHTML = '<li>該当なし</li>';
      return;
    }
    items.forEach((it) => {
      const li = document.createElement('li');
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.textContent = it.name + '(' + it.code + ')';
      btn.addEventListener('click', () => onPick(it));
      li.appendChild(btn);
      listEl.appendChild(li);
    });
  }

  $('bank-search-btn').addEventListener('click', async () => {
    msg('');
    try {
      const data = await fetchBanks($('bank-q').value);
      const items = data.banks || data.items || data || [];
      renderList($('bank-list'), items, (bank) => {
        selectedBankCode = String(bank.code || '');
        selectedBankName = String(bank.name || '');
        if (bankCodeInput) bankCodeInput.value = selectedBankCode;
        if (bankNameInput) bankNameInput.value = selectedBankName;
        $('bank-selected-name').textContent = selectedBankName;
        $('bank-selected-code').textContent = selectedBankCode;
        $('bank-selected').style.display = '';
        $('branch-search-area').style.display = '';
        $('bank-list').innerHTML = '';
      });
    } catch (e) { msg('検索に失敗しました: ' + e.message); }
  });

  $('bank-reset-btn').addEventListener('click', () => {
    selectedBankCode = '';
    selectedBankName = '';
    if (bankCodeInput)   bankCodeInput.value = '';
    if (bankNameInput)   bankNameInput.value = '';
    if (branchCodeInput) branchCodeInput.value = '';
    if (branchNameInput) branchNameInput.value = '';
    $('bank-selected').style.display = 'none';
    $('branch-selected').style.display = 'none';
    $('branch-search-area').style.display = 'none';
    $('branch-list').innerHTML = '';
  });

  $('branch-search-btn').addEventListener('click', async () => {
    msg('');
    if (!/^\d{4}$/.test(selectedBankCode)) { msg('先に金融機関を選んでください'); return; }
    try {
      const data = await fetchBranches(selectedBankCode, $('branch-q').value);
      const items = data.branches || data.items || data || [];
      renderList($('branch-list'), items, (br) => {
        const code = String(br.code || '');
        const name = String(br.name || '');
        if (branchCodeInput) branchCodeInput.value = code;
        if (branchNameInput) branchNameInput.value = name;
        $('branch-selected-name').textContent = name;
        $('branch-selected-code').textContent = code;
        $('branch-selected').style.display = '';
        $('branch-list').innerHTML = '';
      });
    } catch (e) { msg('検索に失敗しました: ' + e.message); }
  });
})();
</script>

エラーハンドリング(失敗時の動き)

BankcodeJPから 429 が返った場合は、
PHPコード
ok:false
を返し、
JSコード
が「時間をおいて再試行」を表示するなど、ユーザーが手入力にフォールバックできるようにする。
curl
タイムアウト・接続エラー時は
ok:false
を返し、画面ロードをブロックしない。

実行結果

ユーザーが一覧から金融機関・支店を選ぶと、指定したフォーム項目にコードと名称が格納される。

まとめ

本記事では、SPIRAL内で完結する構成として、中継用のダミーフォームに
curl
でBankcodeJPを呼ぶPHPを貼り付け、入力フォーム側のJSからそのダミーフォームに
fetch
する流れを示しました。
file_get_contents
等は禁止ですが、
curl_*
は利用可能なため、APIキーをサーバ側に隠したまま外部API連携が実現できます。
不具合時は、fetchのステータス、
<script id="bankcodejp-result">
の中身、BankcodeJP ダッシュボード、禁止関数一覧の順に確認してください。
解決しない場合はこちら コンテンツに関しての
要望はこちら