序文 ~数行とは何だったのか~
「WebPay」サービスの終了とイプシロン決済サービスへの移行方法
で、「イプシロン決済サービスも数行コードで組込可能です」と、ご紹介いただいたページがこちら。
イプシロン開発者向け情報:イプシロンCGI利用ガイド
// HTTPリクエスト実行
$response = $request->send();
(白目)
「営業の約束」から「実装されたコード」へ
HTTP_Request2やXML_SerializerなどのPEARライブラリを強制させられたり
(まあ、環境に依存する関数を極力使用しない為に使用したものと思いたい)、
応答に使用する文字コードがUTF-8だかShift-JISだか混在してたり、
「& new XML_Unserializer();」という記述に一抹の不安を覚えたり、
ページ記載とは別のパラメータが帰ってきてしまったりと、まあ色々。
PHP開発者が退職されてしまったのだろうか……。
そこで、外部ライブラリに頼らないソースを、勉強を兼ねて自前で組んだ。
【重要】おことわり
この記事の内容はポエムであり、ジョークでもある。
下記のソースはテスト環境では動作したが、本番環境で動作する保証はないし、試してもいない。
従い、補償が伴う実運用では、会社より提供されているページのサンプルコードを使うべきだ。
(無いと思うが)このソースの使用・転用・改造などで生じた責任を、当方は負わない。
……というより、私もまだセキュリティ面を考慮したソースを書くスキルが足りていない。
本当に勉強の過程で組んだソースなので、以下も十分にクソコードではある。
(PSRコーディング規約無視してたりするし)
怪しい点があれば、連絡頂けると嬉しいです。
都度課金・クレジットカードのみ対応のソース
<?php
//****************
//test_func_payment.php
//テスト環境の決済システムを叩くためのユーザ関数群
//規模の小さいアプリを想定し、このままで扱えるように書いたが、
//実際には、クラスに書き直したほうが良いと思うぞ。
//****************
define('EPSILON_CODE','11223344'); // イプシロン契約コード
define('BETA','beta.epsilon.jp'); // テスト環境ドメイン
define('RELEASE','keiyakuzi.mail.kisaino.domain'); // 本番環境ドメイン
// 本番環境・テスト環境切り替えはここで行う
define('CGI_SERVER',BETA);
/**
* 決済前、EPSILONに送信する情報を、相手側CGIが食べやすいように加工する
* クレジット決済以外も導入したけりゃ、マニュアル突き合わせてこの関数を弄ろう
*
* @param array $order_data
* @return array EPSILONが求めているデータを配列にまとめたもの
* @throws Exception 必ず注文データ入れてくれ
*/
function epsilon_make_post_data( $order_data = NULL ){
if(!$order_data){throw new Exception('注文データが存在しない');}
return [
'version' => '2',
'contract_code' => EPSILON_CODE,
'user_id' => mb_strimwidth($order_data['id'], 0, 63),
'user_name' => mb_strimwidth($order_data['name'], 0, 63),
'user_mail_add' => mb_strimwidth($order_data['mail'], 0, 127),
'item_code' => mb_strimwidth($order_data['item_code'], 0, 63),
'item_name' => mb_strimwidth($order_data['item_name'], 0, 63),
//注文固有ID(日付+ランダム10~99)
'order_number' => date('YmdHis') . rand(10,99),
// 決済区分 現在はクレジットカードのみ
'st_code' => '11000-0000-00000-00000-00000-00000-00000',
'mission_code' => '1', // 課金区分 (1:一回のみ 2~10:月次課金)
'item_price' => mb_strimwidth($order_data['item_price'], 0, 8),
'process_code' => '1', // 処理区分 月次課金をご利用にならない場合は1:初回課金
'memo1' => 'testMemo1',
'memo2' => 'testMemo2',
'xml' => '1',
'character_code' => 'UTF8'
];
}
/**
* EPSILONにPOSTで情報を送信する
* 第二引数で送信先サーバを必ず指定。
* 決済前のユニークURL取得はbefore, 決済後の注文データ取得はafter。
*
* @param array $send イプシロンに送信するデータ
* @param string $flg_url 送信先CGIのフラグ 'before' or 'after'
* @return string xmlデータ
* @throws Exception 情報送信エラー
*/
function send_post_epsilon( $data = array(), $flg_url = '' ){
mb_language('Japanese');
mb_internal_encoding('UTF-8');
// ヘッダで、相手方に送信フォーマットとデータの長さを伝える
$header = [
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: '.strlen($data)
];
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => implode('\r\n', $header),
'content' => http_build_query($data, '', '&'),
'max_redirects' => 3, // リダイレクトの最大回数 def:3
'timeout' => '20', // タイムアウトの秒数指定
]
]);
if($flg_url === 'before'){
$url = 'https://'. CGI_SERVER .'/cgi-bin/order/receive_order3.cgi';
}elseif($flg_url === 'after'){
// 結果問い合わせ用URL CGI-2利用(basicAuth不要、設定画面からのIP制限有)
// サンプルソースのCGI-1はIP制限無し、basicAuthのみで制限かけてるって恐い気がする……
$url = 'https://'. CGI_SERVER .'/cgi-bin/order/getsales2.cgi';
}else{
throw new Exception('決済サーバURLの指定が異常');
}
$res = file_get_contents($url, false, $context);
if(!$res){throw new Exception('決済サーバに情報が送信できない');}
return (string)$res;
}
/**
* responseで帰ってきたXMLをパースして、一次元連想配列に変換する
* 注:allow_url_fopen が Offだとsimplexml_load_string()が使用できない。
* php.ini を確認し、Offになっていた場合はcURLで色々してくれ(多分cURLのが速い)
*
* @param string $xml
* @return array $array_res
* @throws Exception XML読み込み異常
*/
function load_xml_array( $xml = '' ){
$obj = simplexml_load_string($xml);
if(!$obj){throw new Exception('決済サーバからの情報を解析できない');}
//受け取ったxmlをjsonに変換してからデコードして配列にするという黒魔術
$json_res =json_encode($obj);
$decode_res = json_decode($json_res,TRUE);
//普通にforeach1段で回すと、無駄な多次元配列になってしまう 例:$arr[0]['result']
//2段で回すことで、添字が文字列のみの一次元連想配列にする 例:$arr['result']
//流石に同じ添字が存在しないことを祈る(API信用できてない)
$array_res = [];
foreach($decode_res['result'] as $key => $val){
$attributes = $val['@attributes'];
foreach( $attributes as $key_attr => $val_attr ){
$array_res[$key_attr] = (string)$val_attr;
}
}
return $array_res;
}
/**
* responseデータからコネクション確立可否を確認し、決済ページへのURLを返す
*
* @param array $array_res
* @return string 決済ページへのURL
* @throws Exception EPSILONから帰ってきたエラーメッセージ
*/
function get_payment_url( $array_res = array() ){
if(!$array_res['result']){ //失敗時の処理
$err_code = $array_res['err_code'];
$err_detail = urldecode($array_res['err_detail']);
$err_msg = '決済データの送信に失敗 code-' . $err_code . ':' . $err_detail . PHP_EOL;
$err_msg .= 'memo1:' . $array_res['memo1'] . PHP_EOL;
$err_msg .= 'memo2:' . $array_res['memo2'] . PHP_EOL;
throw new Exception(mb_convert_encoding($err_msg, "UTF-8", "auto"));
}//成功時の処理
return urldecode($array_res['redirect']);
}
/**
* 決済処理完了時の注文データから、必要な分を取得
* 詳細なパラメータはこちらから確認
* http://www.epsilon.jp/developer/each_time.html
* と思ったけど上のパラメータとは違うものが返ってくる。こわい
*
* @param array $array_res
* @return array 必要なデータ
*/
function get_data_after_payment( $array_res = array() ){
return [
'state' => $array_res['state'],
'credit_time' => $array_res['credit_time'],
'order_number' => $array_res['order_number'],
'payment_code' => $array_res['payment_code'],
];
}
<?php
require_once(dirname(__FILE__) . '/test_func_payment.php');
//メインプログラム try-catchで運用する想定
try{
$trans_code = (string)filter_input( INPUT_GET, 'trans_code' );
//$_GETのtrans_codeにデータが無ければ、決済前と判断。CGIを叩いて決済用ユニークURLを発行
//todo: この辺危うい
if(!$trans_code){
//注文情報サンプル
$order = [
'id' => 'testID',
'name' => 'テスト氏名',
'mail' => '[email protected]',
'item_code' => 'testCandy',
'item_name' => 'テスト例の飴',
'item_price' => '1200',
];
$data = epsilon_make_post_data($order);
$req_xml = send_post_epsilon($data, 'before');
$req_array = load_xml_array($req_xml);
$payment_url = get_payment_url($req_array);
echo "<pre>" . PHP_EOL;
echo $payment_url . PHP_EOL;
var_dump($req_array);
echo PHP_EOL . "決済用URL発行</pre>" . PHP_EOL;
//$_GETのtrans_codeにデータがあれば、決済後だと判断。注文データの取得に入る
}else{
$data = [
'contract_code' => EPSILON_CODE,
'trans_code' => $trans_code
];
$req_xml = send_post_epsilon($data, 'after');
$req_array = load_xml_array($req_xml);
$payment_data = get_data_after_payment($req_array);
echo "<pre>" . PHP_EOL;
var_dump($payment_data);
echo PHP_EOL . "--------------------" . PHP_EOL;
var_dump($req_array);
echo PHP_EOL . "決済後情報確認</pre>" . PHP_EOL;
}
}catch (Exception $e) {
echo $e->getMessage();
}
実行結果のサンプル
array(2) {
["result"]=>
string(1) "1"
["redirect"]=>
string(97) "任意のURL"
}
array(26) {
["last_update"]=>
string(23) "2017-04-17+19%3A49%3A58"
["user_mail_add"]=>
string(21) "※ メールアドレスのような何か"
["state"]=>
string(1) "1"
["credit_time"]=>
string(23) "2017-04-17+19%3A50%3A20"
["trans_code"]=>
string(6) "※ イプシロンで発行する取引単位のユニークIDのような何か"
["mission_code"]=>
string(1) "1"
["credit_flag"]=>
string(1) "0"
["item_price"]=>
string(4) "1200"
["payment_code"]=>
string(1) "1"
["item_code"]=>
string(12) "testItemCode"
["order_number"]=>
string(16) "※ 固有の注文番号のような何か "
["st_code"]=>
string(40) "11000-0000-00000-00000-00000-00000-00000"
["memo1"]=>
string(9) "testMemo1"
["contract_code"]=>
string(8) "※ イプシロン契約コードのような何か"
["item_name"]=>
string(45) "※ アイテム名のような何か"
["pay_time"]=>
string(1) "1"
["user_name"]=>
string(54) "※ ユーザ名のような何か"
["process_code"]=>
string(1) "1"
["keitai"]=>
string(1) "0"
["due_date"]=>
string(0) ""
["card_st_code"]=>
string(2) "10"
["add_info"]=>
string(0) ""
["user_id"]=>
string(6) "※ ユーザIDのような何か"
["memo2"]=>
string(9) "testMemo2"
["currencyid"]=>
string(3) "JPY"
["langid"]=>
string(2) "ja"
}