PAYATHON 2026

PayMaya integration in PHP:使用 cURL 创建客户无响应

支付老李

结论

请求没有返回内容,是两个问题共同导致的:

  1. 请求头声明内容是 JSON,实际却通过 http_build_query() 发送了 application/x-www-form-urlencoded 格式的数据。
  2. 代码启用了 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 凭据,也应立即在管理后台撤销或轮换。凭据应通过环境变量保存,不要继续写入源代码或提交到版本库。

备注:内容仅供参考。