PayMaya integration in PHP:使用 cURL 创建客户无响应
结论
请求没有返回内容,是两个问题共同导致的:
- 请求头声明内容是 JSON,实际却通过
http_build_query()发送了application/x-www-form-urlencoded格式的数据。 - 代码启用了
CURLOPT_FAILONERROR。PayMaya 返回400、401等错误状态时,curl_exec()会直接返回false,因此错误响应正文看起来像是“空内容”。
请求体应使用 json_encode() 构造。同时,要在关闭 cURL 连接之前读取 curl_error()、HTTP 状态码和原始响应。
正确发送 JSON 请求
下面的示例沿用原接口地址,并修正了请求体、认证和错误处理:
<?php
class PayMayaCustomerService
{
private $secretKey;
public function __construct($secretKey)
{
$this->secretKey = $secretKey;
}
public function createCustomer()
{
$url = 'https://pg-sandbox.paymaya.com/payments/v1/customers';
$body = [
'firstName' => 'Ysa',
'middleName' => 'Cruz',
'lastName' => 'Santos',
'birthday' => '1987-10-10',
'sex' => 'F',
'contact' => [
'phone' => '+63(2)1234567890',
'email' => 'ysadcsantos@gmail.com',
],
'billingAddress' => [
'line1' => '9F Robinsons Cybergate 3',
'line2' => 'Pioneer Street',
'city' => 'Mandaluyong City',
'state' => 'Metro Manila',
'zipCode' => '12345',
'countryCode' => 'PH',
],
// 如果接口要求 metadata 是 JSON object,应发送 {} 而不是 []
'metadata' => (object) [],
];
$jsonBody = json_encode($body);
if ($jsonBody === false) {
throw new RuntimeException(
'JSON encode failed: ' . json_last_error_msg()
);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/json',
],
// 相当于 Authorization: Basic base64(secretKey + ":")
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->secretKey . ':',
CURLOPT_POSTFIELDS => $jsonBody,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
// 调试阶段不要启用,否则 4xx/5xx 响应会被转换为 false
CURLOPT_FAILONERROR => false,
]);
$response = curl_exec($ch);
$curlErrorNumber = curl_errno($ch);
$curlError = curl_error($ch);
$httpStatus = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
throw new RuntimeException(
sprintf(
'cURL request failed (%d): %s',
$curlErrorNumber,
$curlError
)
);
}
$decodedResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(
sprintf(
'PayMaya returned HTTP %d with a non-JSON response: %s',
$httpStatus,
$response
)
);
}
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new RuntimeException(
sprintf(
'PayMaya returned HTTP %d: %s',
$httpStatus,
$response
)
);
}
return $decodedResponse;
}
}
调用方式如下:
$service = new PayMayaCustomerService(getenv('PAYMAYA_SECRET_KEY'));
try {
$customer = $service->createCustomer();
print_r($customer);
} catch (RuntimeException $e) {
echo $e->getMessage();
}
原代码中的格式错误
原代码设置了以下请求头:
Content-Type: application/json
实际请求体却使用了:
http_build_query($body)
http_build_query() 生成的是类似下面这样的表单数据:
firstName=Ysa&middleName=Cruz&contact%5Bphone%5D=...
这并不是 JSON。既然接口要求 application/json,请求体就必须使用:
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
还要留意 PHP 空数组的编码结果。下面的写法:
'metadata' => []
会得到:
"metadata": []
如果接口将 metadata 定义为对象,应改成:
'metadata' => (object) []
编码结果才会是:
"metadata": {}
为什么 CURLOPT_FAILONERROR 会让响应看起来为空
启用以下选项后:
curl_setopt($ch, CURLOPT_FAILONERROR, true);
服务器返回 HTTP 400 或更高的错误状态时,curl_exec() 通常会返回 false。此时再执行:
return json_decode($response);
无法得到有意义的结果,PayMaya 返回的具体校验错误也随之被忽略。
调试 API 时应关闭该选项,并检查响应、cURL 错误和 HTTP 状态码:
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
取得这些信息后再调用 curl_close()。
Basic Authentication 的处理
不要长期在代码中保存手工计算的 Authorization 请求头。cURL 可以直接生成 Basic Authentication:
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $secretKey . ':',
如果必须手动构造,请求头通常写成:
'Authorization: Basic ' . base64_encode($secretKey . ':')
究竟应使用公钥还是密钥,需要以当前调用的 PayMaya API 文档为准。创建客户属于服务端操作时,通常不能把密钥暴露给浏览器或移动端。
正确处理 PayMayaSDK 的 namespace
不要修改 SDK 文件,也不要注释掉其中的 namespace。如果 SDK 声明了:
namespace PayMaya;
业务代码应通过 Composer 自动加载,并导入完整类名:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PayMaya\PayMayaSDK;
PayMayaSDK::getInstance()->initCheckout(
$publicKey,
$secretKey,
'SANDBOX'
);
也可以直接使用完整限定类名:
\PayMaya\PayMayaSDK::getInstance()->initCheckout(
$publicKey,
$secretKey,
'SANDBOX'
);
如果自己的类也有 namespace,例如:
namespace App\Services;
完整写法如下:
<?php
namespace App\Services;
use PayMaya\PayMayaSDK;
class PaymentService
{
public function initialize($publicKey, $secretKey)
{
PayMayaSDK::getInstance()->initCheckout(
$publicKey,
$secretKey,
'SANDBOX'
);
}
}
遇到 Class "PayMaya\PayMayaSDK" not found 时,应检查:
- 是否加载了
vendor/autoload.php; - SDK 实际声明的 namespace 和类名;
composer.json中的自动加载配置;- 已安装的 SDK 版本是否与参考代码一致。
修改第三方 SDK 的源文件只能暂时掩盖自动加载问题,依赖更新后还可能再次失效。
还需要检查的事项
如果改用 JSON 后请求仍然失败,先输出 HTTP 状态码和原始响应,再逐项检查:
- Sandbox 地址是否仍是当前账户适用的 API 地址;
- API 密钥是否属于 Sandbox 环境;
- Basic Authentication 是否使用了正确的服务端密钥;
phone、birthday、countryCode等字段格式是否符合接口当前要求;- PHP 是否启用了 cURL,以及 TLS 所需的 CA 证书是否可用;
- 服务器是否能够访问 PayMaya 的 HTTPS 地址。
问题代码中已经暴露了公钥、密钥和 Basic Authentication 内容。即使这些只是 Sandbox 凭据,也应立即在管理后台撤销或轮换。凭据应通过环境变量保存,不要继续写入源代码或提交到版本库。
备注:内容仅供参考。
