CakePHP+Paypal決済モジュール

技術情報・ノウハウ

CakePHPに組み込むPaypal決済(エクスプレス チェックアウト)のモジュールを御紹介いたします。

Paypalの決済処理をCakePHPのモジュールにしました

Paypalには様々な決済方法が用意されていますが、最もスタンダードなソリューションは「エクスプレスチェックアウト(Express Checkout)」です。「ウェブペイメントスタンダード」も機能的にはほぼ同じですが、カートシステムを構築するのであればAPIの使えるエクスプレスチェックアウトを使う方がよいでしょう。

Paypalの決済処理はやや複雑です。エクスプレスチェックアウトを使ったCakePHP用のモジュールを作成しましたので、ご紹介いたします。

※ご利用の場合にはPaypalのマニュアルも確認しながら行ってください。

Modelの用意

まず、Model「Paypal.php」を用意します。このモデルを用いて、決済の処理を行います。
PearライブラリRequestを読み込んでいるので、Pearも用意してください。

<?php
class Paypal extends AppModel {

	var $name = 'Paypal';
	var $useTable = false;

	var $apiServer = PAYPAL_API_SERVER;
	var $expressCheckoutUrl = PAYPAL_EXPRESS_CHECKOUT_URL;

	var $version = '63.0';
	var $username = PAYPAL_API_USERNAME;
	var $password = PAYPAL_API_PASSWORD;
	var $signature = PAYPAL_API_SIGNATURE;
	var $returnURL;
	var $cancelURL;


	var $errorMsg = array();

	function __construct() {
		vendor('Request');
		$this->returnURL = FULL_BASE_URL.'/mypages/confirm';
		$this->cancelURL = FULL_BASE_URL.'/mypages/cancel';
	}

	function setExpressCheckout ($item_amount, $request='Sale', $options=array()) {
		$params = array(
			'METHOD' => 'SetExpressCheckout',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'PAYMENTREQUEST_0_AMT' => $item_amount,
			'PAYMENTREQUEST_0_CURRENCYCODE' => 'JPY',
			'PAYMENTREQUEST_0_ITEMAMT' => $item_amount,
			'PAYMENTREQUEST_0_PAYMENTACTION' => $request,
			'LOCALECODE' => 'JP'
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);
		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);

			if ($result['ACK'] == 'Success') {
				$this->expressCheckoutUrl .= "&token={$result['TOKEN']}";
				$this->expressCheckoutUrl .= '&useraction=commit';
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Set - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}

	}

	function getExpressCheckoutDetails($token=null, $options=array()) {
		$params = array(
			'METHOD' => 'GetExpressCheckoutDetails',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'TOKEN' => $token
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Get - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}
	}

	function doExpressCheckout($token, $payerId, $amount, $request='Sale', $options=array()) {
		$params = array(
			'METHOD' => 'DoExpressCheckoutPayment',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'TOKEN' => $token,
			'PAYERID' => $payerId,
			'PAYMENTREQUEST_0_AMT' => $amount,
			'PAYMENTREQUEST_0_CURRENCYCODE' => 'JPY',
			'PAYMENTREQUEST_0_PAYMENTACTION' => $request
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {

				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Do - {$params['PAYERID']} - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}
	}

	function doCapture($transactionId, $amount=0, $complete=false, $options=array()) {
		$params = array(
			'METHOD' => 'DoCapture',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'AUTHORIZATIONID' => $transactionId,
			'AMT' => $amount,
			'CURRENCYCODE' => 'JPY',
			'COMPLETETYPE' => ($complete) ? 'Complete' : 'NotComplete'
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log('DoCap - '.$result['CORRELATIONID'].' - '.$result['L_ERRORCODE'.$i], 'paypal');
					}
				}
				return false;
			}
		}
	}

	function doVoid($transactionId, $options=array()) {
		$params = array(
			'METHOD' => 'DoVoid',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'AUTHORIZATIONID' => $transactionId,
			'CURRENCYCODE' => 'JPY',
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log('DoVoid - '.$params['AUTHORIZATIONID'].' - '.$result['L_ERRORCODE'.$i], 'paypal');
					}
				}
				return false;
			}
		}
	}

}
?>

PaypalのIP、パスワードなどの設定は、Bootstrap.phpで行うと良いと思います。

if (preg_match ('|cake_dev|',ROOT)){ //テスト環境
	define('PAYPAL_API_SERVER', 'https://api-3t.sandbox.paypal.com/nvp');
	define('PAYPAL_EXPRESS_CHECKOUT_URL', 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout');
	define('PAYPAL_API_USERNAME', '***_biz_api1.***.com');
	define('PAYPAL_API_PASSWORD', '***');
	define('PAYPAL_API_SIGNATURE', '***');
}else{
	define('PAYPAL_API_SERVER', 'https://api-3t.paypal.com/nvp');
	define('PAYPAL_EXPRESS_CHECKOUT_URL', 'https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout');
	define('PAYPAL_API_USERNAME', '***.***.com');
	define('PAYPAL_API_PASSWORD', '***');
	define('PAYPAL_API_SIGNATURE', '***');
}

 

Controllerでの処理

Controllerでは、まずさきほどのModelを呼び出します。

var $uses = array('Paypal'); 

大まかな処理の流れは、

  1. カートに商品を入れる(チェックアウト取引の開始)
  2. 確認画面で商品の確認をする(Paypalへリダイレクト)
  3. Paypalに飛んで決済する
  4. 完了画面に戻る(決済情報を取得)

となります。

カートページから確認ページに遷移する直前にチェックアウト取引の開始をします。問題なければ、確認ページへリダイレクトします。

if ($setResult = $this->Paypal->setExpressCheckout($amount, 'Sale', $setOptions)) {
	$this->params['data']['paypal']['express_checkout_url'] = $this->Paypal->expressCheckoutUrl;
} else {
	$this->set('ppErrors', $this->Paypal->errorMsg);
}

$this->Session->write('mypage_cart', $this->params['data']);
$this->redirect('/mypages/confirm');

また、このとき$setOptionsに、オプションをセットすることができます。例えば、下記のように書くことができます。$setOptionsと、setExpressCheckoutの$paramsで、Paypalに渡すデータをセットしているので、構築するカートに合わせて調整してください。

$setOptions = array(
	'NOSHIPPING' => '1',
	'ALLOWNOTE' => '0',
	'L_PAYMENTREQUEST_0_NAME0' => $this->data['Item']['title'],
	'L_PAYMENTREQUEST_0_DESC0' => $this->data['Item']['description'],
	'L_PAYMENTREQUEST_0_AMT0' => $this->data['Item']['amount'],
	'RETURNURL' => FULL_BASE_URL.'/mypages/confirm',
	'CANCELURL' => FULL_BASE_URL.'/mypages/cancel'
);

次に、確認ページですが、カートの情報を確認して「決済へ」というボタンをクリックしたときの処理を走らせた後に、チェックアウトURLにリダイレクトさせます。

$this->redirect($this->data['paypal']['express_checkout_url']);
exit;

これでユーザはPaypalに移動し、決済を行います。決済が終了すると、returnURL に指定したURLにParam付きで帰ってきます。

さあ、最後の確認処理です。tokenを確認し、値があれば保存用のパラメータに内容をセットしています。

if ($getResult = $this->Paypal->getExpressCheckoutDetails($this->params['url']['token'])) {

	$this->data['Item']['pp_name'] = $getResult['FIRSTNAME'].' '.$getResult['LASTNAME'];
	$this->data['Item']['pp_email'] = $getResult['EMAIL'];
	$this->data['Item']['pp_amt'] = $getResult['AMT'];

	if ($doResult = $this->Paypal->doExpressCheckout($getResult['TOKEN'], $getResult['PAYERID'], $getResult['AMT'])) {
		$this->data['Item']['transaction_id'] = $doResult['PAYMENTINFO_0_TRANSACTIONID'];
	}
} else {
	// エラー
	die('error unknown');
}

あとは返ってきた内容を保存したり、ユーザに完了メールを送信して処理は終了となります。

※このControllerでは、PaypalモデルのなかでdoCaptureとdoVoidを使用していません。
doCaptureは、発送後に支払いを回収するため、doVoidはキャンセルのために呼び出します。

参考

PayPal API導入・活用ガイド
PayPal Express Checkout Integration Guide(英語マニュアル:PDF)
Paypal APIの最新バージョンの確認