CSDN有点恶心了,不登录不给复制,还每次都弹窗
准备:需要有一个商家账户,测试环境可以使用PayPal的沙盒账户进行测试,网址: https://developer.paypal.com/
这里我没有使用PayPal提供的SDK,而是自己封装了一个类。
PayPal Payment v1的API已经被v2替代,所以这里只使用v2接口创建订单。各个接口的参数请参照PayPal官方文档。地址:https://developer.paypal.com/docs/api/overview/
<?php
declare (strict_types=1);
namespace PayPal;
use \Exception;
/**
* Class PP
* PayPal相关API,所有接口失败都会抛出异常
* @uses 实例化:$pp = new PayPal(string $clientId, string $clientSecret, array $config);
* @uses 添加发货信息:$pp->addTrackers(array $trackers): bool
* @uses 查询发货信息:$pp->getTracker(string $transactionId, string $trackingNumber): array
* @uses 执行收款:$pp->executePayment(string $paymentId, string $payerId): array
* @uses 执行退款:$pp->executeRefund(string $transactionId, float $amount, string $currency = 'USD', string $reason = ''): array
* @uses 创建PayPal订单v2:$pp->createOrder(array $params): array
* @uses 执行收款v2:$pp->captureOrder(string $payPalOrderId): array
* @uses 获取订单详情v2:$pp->getOrder(string $payPalOrderId): array
* @author Lushaoming<lusm@sz-bcs.com.cn>
* @date 2020-03-12
*/
class PayPal
{
private $clientId;
private $clientSecret;
private $config;
private $cipher;
private $error;
private static $expiryBufferTime = 120;
const SDK_NAME = 'PayPal-PHP-SDK';
const SDK_VERSION = '1.14.0';
/**
* PP constructor.
* @param string $clientId
* @param string $clientSecret
* @param array $config See default configurations in $this->_getDefaultConfig()
*/
public function __construct($clientId, $clientSecret, $config = [])
{
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->config = $this->_mergeConfig($config);
$this->cipher = new Cipher($clientSecret);
}
/**
* Get Access Token.
* If caching is enabled, first check whether the cached token has expired.
* If it does not expire, use it directly. If it expires, obtain the token again
* @return string
* @throws \Exception
*/
private function _getAccessToken()
{
if ($this->config['cache.enabled'] && file_exists($this->config['cache.FileName'])) {
$data = file_get_contents($this->config['cache.FileName']);
if ($data) {
$data = json_decode($data, true);
if (isset($data[$this->clientId]) &&
$data[$this->clientId]['accessTokenEncrypted'] &&
($data[$this->clientId]['tokenCreateTime'] + $data[$this->clientId]['tokenExpiresIn'] - self::$expiryBufferTime > time() ))
{
return $this->cipher->decrypt($data[$this->clientId]['accessTokenEncrypted']);
}
}
}
// 重新获取token
return $this->_updateAccessToken();
}
/**
* Add tracking information to PayPal
* @param array $trackers
* @return bool
* @throws Exception
*/
public function addTrackers(array $trackers)
{
$items = [];
foreach ($trackers as $tracker) {
$items[] = [
'transaction_id' => $tracker['transaction_id'],
'tracking_number' => $tracker['tracking_number'],
'status' => $tracker['status'],
'carrier' => $tracker['carrier'],
'carrier_name_other' => $tracker['carrier_name_other'],
'shipment_date' => $tracker['shipment_date']
];
}
if (count($items) == 0) {
$this->setError('No item.');
return false;
}
$param = [
'trackers' => $items
];
$this->_execute(
$this->_getApiDomain() . '/v1/shipping/trackers-batch',
'POST',
$this->_toJSON($param),
$this->_dealHeaders($this->_getRequestHeaders())
);
return true;
}
/**
* @param string $transactionId 交易号
* @param string $trackingNumber 运输号
* @throws \Exception
* @return array
*/
public function getTracker($transactionId, $trackingNumber)
{
$url = $this->_getApiDomain() . "/v1/shipping/trackers/{$transactionId}-{$trackingNumber}";
$headers = $this->_getRequestHeaders();
$result = $this->_execute($url, 'GET', "", $this->_dealHeaders($headers));
return $this->_toArray($result);
}
/**
* @param string $paymentId
* @param string $payerId
* @return array|mixed
* @throws \Exception
*/
public function executePayment($paymentId, $payerId)
{
$param = ['payer_id' => $payerId];
$result = $this->_execute(
$this->_getApiDomain() . "/v1/payments/payment/{$paymentId}/execute",
'POST',
$this->_toJSON($param),
$this->_dealHeaders($this->_getRequestHeaders())
);
return $this->_toArray($result);
}
/**
* @param string $transactionId
* @param float $amount
* @param string $currency
* @param string $reason
* @return array|mixed
* @throws Exception
*/
public function executeRefund($transactionId, $amount, $currency = 'USD', $reason = '')
{
$param = [
'amount' => [
'currency' => $currency,
'total' => $amount
],
'reason' => $reason
];
$result = $this->_execute(
$this->_getApiDomain() . "/v1/payments/sale/$transactionId/refund",
'POST',
$this->_toJSON($param),
$this->_dealHeaders($this->_getRequestHeaders())
);
return $this->_toArray($result);
}
/**
*
* @param array $params
* @return array|mixed
* @throws Exception
*/
public function createOrder(array $params)
{
$result = $this->_execute(
$this->_getApiDomain()."/v2/checkout/orders",
'POST',
json_encode($params),
$this->_dealHeaders($this->_getRequestHeaders())
);
return $this->_toArray($result);
}
/**
* @param $payPalOrderId
* @return array|mixed
* @throws Exception
*/
public function getOrder($payPalOrderId)
{
$result = $this->_execute(
$this->_getApiDomain()."/v2/checkout/orders/{$payPalOrderId}",
'GET',
'',
$this->_dealHeaders($this->_getRequestHeaders())
);
return $this->_toArray($result);
}
/**
* @param string $payPalOrderId Approval paypal order id
* @return array|mixed
* @throws Exception
*/
public function captureOrder($payPalOrderId)
{
$result = $this->_execute(
$this->_getApiDomain()."/v2/checkout/orders/{$payPalOrderId}/capture",
'POST',
'',
$this->_dealHeaders($this->_getRequestHeaders())
);
return $this->_toArray($result);
}
/**
* Get request headers
* @return array
* @throws \Exception
*/
private function _getRequestHeaders()
{
return [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->_getAccessToken()
];
}
/**
* Get a new access token
* @return string
* @throws \Exception
*/
private function _updateAccessToken()
{
$headers = array(
"User-Agent" => $this->_getUserAgent(self::SDK_NAME, self::SDK_VERSION),
"Authorization" => "Basic " . base64_encode($this->clientId . ":" . $this->clientSecret),
"Accept" => "*/*",
);
$params = [
'grant_type' => 'client_credentials'
];
$result = $this->_execute($this->_getOauthUrl(), 'POST', http_build_query($params), $this->_dealHeaders($headers));
$result = json_decode($result, true);
// 写入文件缓存
if ($this->config['cache.enabled']) {
// 注意不能覆盖其他账号的缓存
$tokens = [];
if (file_exists($this->config['cache.FileName'])) {
$data = file_get_contents($this->config['cache.FileName']);
if ($data) {
$tokens = $this->_toArray($data);
}
}
$tokens[$this->clientId] = [
'clientId' => $this->clientId,
'accessTokenEncrypted' => $this->cipher->encrypt($result['access_token']),// token加密存储
'tokenCreateTime' => time(),
'tokenExpiresIn' => $result['expires_in']
];
if (!file_put_contents($this->config['cache.FileName'], json_encode($tokens))) {
throw new \Exception("Failed to write cache, path: " . $this->config['cache.FileName']);
};
}
return $result['access_token'];
}
/**
* Array or object to json
* @param $data
* @param int $options
* @return mixed|string
*/
private function _toJSON($data, $options = 0)
{
// Because of PHP Version 5.3, we cannot use JSON_UNESCAPED_SLASHES option
// Instead we would use the str_replace command for now.
// TODO: Replace this code with return json_encode($data, $options | 64); once we support PHP >= 5.4
if (version_compare(phpversion(), '5.4.0', '>=') === true) {
return json_encode($data, $options | 64);
}
return str_replace('\\/', '/', json_encode($data, $options));
}
/**
* Json to array
* @param $data
* @return array|mixed
*/
private function _toArray($data)
{
if (!$data) return [];
return json_decode($data, true);
}
/**
* 证书下载地址:http://curl.haxx.se/ca/cacert.pem
* @param $url
* @param $method
* @param $data
* @param array $headers
* @param array $options
* @return mixed
* @throws \Exception
*/
private function _execute($url, $method, $data, $headers = [], $options = [])
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// 不直接输出
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//Determine Curl Options based on Method
switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
case 'PUT':
case 'PATCH':
case 'DELETE':
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
}
// curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'parseResponseHeaders'));
if (strpos($this->_getApiDomain(), "https://") === 0) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
}
// if ($caCertPath = $this->getCACertFilePath()) {
// curl_setopt($ch, CURLOPT_CAINFO, $caCertPath);
// }
//Execute Curl Request
$result = curl_exec($ch);
//Retrieve Response Status
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
//Retry if Certificate Exception
if (curl_errno($ch) == 60) {
$this->_writeLog("Invalid or no certificate authority found - Retrying using bundled CA certs file");
curl_setopt($ch, CURLOPT_CAINFO, $this->getCACertFilePath());
$result = curl_exec($ch);
//Retrieve Response Status
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
}
//Throw Exception if Retries and Certificates doenst work
if (curl_errno($ch)) {
$ex = new \Exception(
curl_error($ch),
curl_errno($ch)
);
curl_close($ch);
throw $ex;
}
// Get Request and Response Headers
// $requestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT);
//Close the curl request
curl_close($ch);
//More Exceptions based on HttpStatus Code
if ($httpStatus < 200 || $httpStatus >= 300) {
$ex = new \Exception(
$result,
$httpStatus
);
$this->_writeLog("Got Http response code $httpStatus when accessing {$url}. " . $result, 'ERROR');
throw $ex;
}
return $result;
}
private function getCACertFilePath()
{
return __DIR__.'/../certs/paypal-cacert.pem';
}
/**
* Get oauth token credential URL
* @return string
*/
private function _getOauthUrl()
{
return $this->_getApiDomain() . "/v1/oauth2/token";
}
/**
* Get API domain based on $this->config['mode']
* @return string
*/
private function _getApiDomain()
{
if ($this->config['mode'] == 'production') return 'https://api.paypal.com';
else return 'https://api.sandbox.paypal.com';
}
/**
* Get user agent
* @param $sdkName
* @param $sdkVersion
* @return string
*/
private static function _getUserAgent($sdkName, $sdkVersion)
{
$featureList = array(
'platform-ver=' . PHP_VERSION,
'bit=' . self::_getPHPBit(),
'os=' . str_replace(' ', '_', php_uname('s') . ' ' . php_uname('r')),
'machine=' . php_uname('m')
);
if (defined('OPENSSL_VERSION_TEXT')) {
$opensslVersion = explode(' ', OPENSSL_VERSION_TEXT);
$featureList[] = 'crypto-lib-ver=' . $opensslVersion[1];
}
if (function_exists('curl_version')) {
$curlVersion = curl_version();
$featureList[] = 'curl=' . $curlVersion['version'];
}
return sprintf("PayPalSDK/%s %s (%s)", $sdkName, $sdkVersion, implode('; ', $featureList));
}
private static function _getPHPBit()
{
switch (PHP_INT_SIZE) {
case 4:
return '32';
case 8:
return '64';
default:
return PHP_INT_SIZE;
}
}
/**
* Deal request headers
* @param array $headers
* @return array
*/
private function _dealHeaders($headers)
{
$ret = [];
foreach ($headers as $k => $v) {
$ret[] = "$k: $v";
}
return $ret;
}
/**
* Merge from default configs and return.
* @param array $config
* @return array
*/
private function _mergeConfig($config)
{
$defaultConfigs = $this->_getDefaultConfig();
foreach ($defaultConfigs as $key => $value) {
if (isset($config[$key])) $defaultConfigs[$key] = $config[$key];
}
return $defaultConfigs;
}
/**
* Get Default configs
* @return array
*/
private function _getDefaultConfig()
{
return [
'mode' => 'live', // sandbox / live
'log.LogEnabled' => 1, // 1 / 0
'log.FileName' => '/www/log/PayPal.log',
'log.LogLevel' => 'INFO', // Sandbox Environments: DEBUG, INFO, WARN, ERROR; Live Environments: INFO, WARN, ERROR
'validation.level' => 'disable',
'cache.enabled' => 1, // 1 / 0
'cache.FileName' => '/www/log/auth.cache'
];
}
private function setError($error)
{
$this->error = $error;
}
public function getError()
{
return $this->error;
}
/**
* Write PayPal log
* @param $log
* @param string $level
*/
private function _writeLog($log, $level = 'INFO')
{
if ($this->config['log.LogEnabled']) {
$message = "[".date('Y-m-d H:i:s') . "][{$level}-".$this->_generateLogId()."]{$log}";
if (!file_put_contents($this->config['log.FileName'], $message . PHP_EOL, FILE_APPEND)) {
// throw new \Exception("Failed to write log, path: " . $this->config['log.FileName']);
};
}
}
/**
* @return string
*/
private function _generateLogId()
{
static $logId;
if (!$logId) {
$logId = mb_substr(md5(time() . mt_rand(100, 999)), 0, 6);
}
return $logId;
}
/**
* Print the debug message
* @param $msg
*/
public static function printMsg($msg)
{
if (is_array($msg)) {
echo '['.date('Y-m-d H:i:s') . ']' . json_encode($msg) . PHP_EOL;
} else {
echo '['.date('Y-m-d H:i:s') . ']' . $msg . PHP_EOL;
}
}
}
- 加密解密类
<?php
namespace Reolink\Payments\PayPal;
/**
* Class Cipher
*
* Helper class to encrypt/decrypt data with secret key
*
* @package Reolink\Payments\PayPal
*/
class Cipher
{
private $secretKey;
/**
* Fixed IV Size
*/
const IV_SIZE = 16;
public function __construct($secretKey)
{
$this->secretKey = $secretKey;
}
/**
* Encrypts the input text using the cipher key
*
* @param $input
* @return string
*/
public function encrypt($input)
{
// Create a random IV. Not using mcrypt to generate one, as to not have a dependency on it.
$iv = substr(uniqid("", true), 0, Cipher::IV_SIZE);
// Encrypt the data
$encrypted = openssl_encrypt($input, "AES-256-CBC", $this->secretKey, 0, $iv);
// Encode the data with IV as prefix
return base64_encode($iv . $encrypted);
}
/**
* Decrypts the input text from the cipher key
*
* @param $input
* @return string
*/
public function decrypt($input)
{
// Decode the IV + data
$input = base64_decode($input);
// Remove the IV
$iv = substr($input, 0, Cipher::IV_SIZE);
// Return Decrypted Data
return openssl_decrypt(substr($input, Cipher::IV_SIZE), "AES-256-CBC", $this->secretKey, 0, $iv);
}
}
近期评论