入力フォーム上で金融機関・支店を検索一覧から選べるようにし、選択結果をフォーム項目へ格納する構成を紹介します。
検索用の中継PHPは、本フォームとは別にSPIRAL内にもう1つ「ダミーフォーム」(見えないダミー項目だけで構成した空フォーム)を作成し、
そのフォームのHTMLソース編集画面に貼り付ける前提です。ダミーフォームのURLがそのままブラウザから叩ける中継エンドポイントになります。
注意点
・ APIキーは
・ 中継用ダミーフォームは、入力項目や確認画面を持たない・メール配信もしない設定にしておき、PHPを貼り付けるためだけの空フォームとして運用してください。
・ 無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
・
・ スパイラルPHPでは
PHPコード(サーバ側)に保持し、
JSコード(クライアント)には置きません。
・ 中継用ダミーフォームは、入力項目や確認画面を持たない・メール配信もしない設定にしておき、PHPを貼り付けるためだけの空フォームとして運用してください。
・ 無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
・
curlのタイムアウト(接続・全体)は短めに設定し、外部API遅延で画面が固まらないようにしてください。
・ スパイラルPHPでは
header()が使えないため、ステータス相当の値はJSON本文の
status・
okフィールドに入れて返します。
実装の概要
事前準備. BankcodeJP で APIキー取得。SPIRAL内に「中継用ダミーフォーム」を1つ作成し、そのHTMLソースに
1. 入力フォーム側の
2. 中継フォームに貼られた
3.
4. ユーザーが候補を選んだら、金融機関コード・名称、支店コード・名称を hidden 等に格納し、本来の入力フォームを送信する。
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バイト) |
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>タグ除去や < → < エスケープをかけても
// 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内で完結する構成として、中継用のダミーフォームに
不具合時は、fetchのステータス、
curlでBankcodeJPを呼ぶPHPを貼り付け、入力フォーム側のJSからそのダミーフォームに
fetchする流れを示しました。
file_get_contents等は禁止ですが、
curl_*は利用可能なため、APIキーをサーバ側に隠したまま外部API連携が実現できます。
不具合時は、fetchのステータス、
<script id="bankcodejp-result">の中身、BankcodeJP ダッシュボード、禁止関数一覧の順に確認してください。