yansongda/pay
A popular PHP payment library for Laravel and other frameworks, integrating Alipay and WeChat Pay with a clean API. Supports common payment flows, refunds, queries, callbacks/notifications, and flexible configuration for multi-app and sandbox use.
Installation:
composer require yansongda/pay:"~3.8.0@beta"
For Laravel projects, use the wrapper package instead:
composer require yansongda/laravel-pay
Apple Pay RSA_v1 token decryption requires PHP >= 8.5; EC_v1 has no such PHP version restriction.
Where to Look First:
web/docs/v3/quick-start/init.md — config bootstrapping and the new enum style.web/docs/v3/apple/ — pay / query / refund / callback / response / plugin pages for the new Apple provider.web/docs/v3/wechat/pay.md and web/docs/v3/wechat/virtual.md — enum usage for _type, PapayScene, ReceiverType, VirtualPayEnv.src/Enum/ — single source of truth for all enums (PayMode, Apple\SubscriptionStatus, Wechat\*, Alipay\*).Basic Configuration:
Define gateway configuration in config/pay.php:
use Yansongda\Pay\Enum\PayMode;
return [
'alipay' => [
'default' => [
'app_id' => env('ALIPAY_APP_ID'),
'app_secret_cert' => env('ALIPAY_APP_SECRET_CERT'),
'app_public_cert_path' => storage_path('certs/alipay/appCertPublicKey.crt'),
'alipay_public_cert_path' => storage_path('certs/alipay/alipayCertPublicKey_RSA2.crt'),
'alipay_root_cert_path' => storage_path('certs/alipay/alipayRootCert.crt'),
'notify_url' => route('alipay.notify'),
],
],
'wechat' => [
'default' => [
'mch_id' => env('WECHAT_MCH_ID'),
'mp_app_id' => env('WECHAT_MP_APP_ID'),
'mini_app_id' => env('WECHAT_MINI_APP_ID'),
'app_id' => env('WECHAT_APP_ID'),
'mch_secret_key_v2' => env('WECHAT_MCH_SECRET_KEY_V2'),
'mch_secret_key' => env('WECHAT_MCH_SECRET_KEY'),
'mch_secret_cert' => storage_path('certs/wechat/apiclient_key.pem'),
'mch_public_cert_path' => storage_path('certs/wechat/apiclient_cert.pem'),
'notify_url' => route('wechat.notify'),
],
],
'apple' => [
'default' => [
'issuer_id' => env('APPLE_ISSUER_ID'),
'bundle_id' => env('APPLE_BUNDLE_ID'),
'api_key_id' => env('APPLE_API_KEY_ID'),
'api_private_key' => storage_path('certs/apple/AuthKey.p8'),
// Optional overrides; bundled Apple public certs are used by default.
// 'apple_root_ca' => storage_path('certs/apple/AppleRootCA-G3.pem'),
// 'apple_intermediate_ca' => storage_path('certs/apple/AppleAAICAG3.pem'),
],
],
'logger' => [
'enable' => env('PAY_LOG_ENABLE', false),
'file' => storage_path('logs/pay.log'),
'level' => env('PAY_LOG_LEVEL', 'debug'),
],
];
mode now uses the PayMode enum:
use Yansongda\Pay\Enum\PayMode;
'mode' => env('PAY_MODE') === 'sandbox'
? PayMode::SANDBOX
: PayMode::NORMAL,
Historical integer values (0 / 1 / 2) are still accepted in configuration, but code comparisons must use the enum methods.
First Use Case: Trigger a payment in a controller:
use Yansongda\Pay\Pay;
public function pay()
{
Pay::config(config('pay'));
$result = Pay::alipay()->web([
'out_trade_no' => 'order_' . time(),
'total_amount' => '10.00',
'subject' => 'Test Payment',
]);
return redirect($result->getRedirectUrl());
}
Callback Handling:
public function notify()
{
Pay::config(config('pay'));
try {
$data = Pay::alipay()->callback(); // Auto-verifies signature
// Process order: $data->out_trade_no, $data->total_amount
return Pay::alipay()->success();
} catch (\Throwable $e) {
return Pay::alipay()->fail();
}
}
// Web payment
$pay = Pay::alipay()->web([
'out_trade_no' => 'order_' . time(),
'total_amount' => '10.00',
'subject' => 'Product Purchase',
'body' => 'Description',
'timeout_express' => '30m',
]);
// Scan payment (QR code)
$pay = Pay::alipay()->scan([
'out_trade_no' => 'order_' . time(),
'auth_code' => $request->input('auth_code'),
'total_amount' => '10.00',
]);
// Mini Program payment
$pay = Pay::wechat()->mini([
'out_trade_no' => 'order_' . time(),
'description' => 'Mini Program Purchase',
'amount' => ['total' => 1000], // Amount in cents
'payer' => ['openid' => $openid],
]);
// JSAPI (H5) payment
$pay = Pay::wechat()->jsapi([
'out_trade_no' => 'order_' . time(),
'body' => 'H5 Purchase',
'total_fee' => 1000,
]);
use Yansongda\Pay\Enum\Apple\SubscriptionStatus;
// Local verification/decryption of an Apple Pay payment token.
// PKCS#7 cert chain + ECDH + AES-256-GCM, no extra Composer dependencies.
$result = Pay::apple()->payToken([
'payment_token' => $request->input('payment_token'),
]);
// Web merchant validation proxy.
// initiative_context is required. TLS mutual auth is passed through via `_http`.
$session = Pay::apple()->merchantSession([
'initiative_context' => 'https://example.com',
'merchant_identifier' => 'merchant.com.example',
'domain_name' => 'example.com',
'display_name' => 'Example Store',
]);
// App Store Server API query
$transaction = Pay::apple()->query([
'_action' => 'transaction',
'transaction_id' => '2000000000000000',
]);
$history = Pay::apple()->query([
'_action' => 'history',
'transaction_id' => '2000000000000000',
]);
// Subscription query. _status accepts SubscriptionStatus or int 1-5 (whitelisted).
$subscriptions = Pay::apple()->query([
'_action' => 'subscriptions',
'_status' => SubscriptionStatus::ACTIVE,
]);
// Apple has no server-side active refund API.
// This queries refund history only.
$refunds = Pay::apple()->refund([
'transaction_id' => '2000000000000000',
]);
// App Store Server Notifications V2 callback:
// verifies JWS x5c chain, nested transaction JWS, bundleId/environment, cert validity.
$notification = Pay::apple()->callback();
cancel() / close() have no Apple equivalent and throw an exception when called.
// Query order status
$status = Pay::alipay()->query(['out_trade_no' => 'order_123']);
// Refund
$refund = Pay::wechat()->refund([
'out_refund_no' => 'refund_' . time(),
'out_trade_no' => 'order_123',
'refund_amount' => 500, // Partial refund
]);
v3.8 introduces enums across providers. Default behavior and wire payloads remain byte-compatible unless you rely on the removed mode constants or getModeEnum().
use Yansongda\Pay\Enum\PayMode;
'config' => [
'mode' => PayMode::SANDBOX,
];
// Before v3.8:
// if ($config->getMode() === Pay::MODE_SANDBOX) { ... }
// v3.8+:
if ($config->getMode()->isSandbox()) {
// ...
}
if ($config->getMode()->isService()) {
// ...
}
Pay::MODE_NORMAL, Pay::MODE_SANDBOX, and Pay::MODE_SERVICE have been removed. Use PayMode::NORMAL, PayMode::SANDBOX, and PayMode::SERVICE.
toArray()['mode'] now returns a PayMode enum, while toJson() / json_encode() still output 0 / 1 / 2.
use Yansongda\Pay\Enum\Wechat\AppType;
use Yansongda\Pay\Enum\Wechat\PapayScene;
use Yansongda\Pay\Enum\Wechat\ReceiverType;
use Yansongda\Pay\Enum\Wechat\VirtualPayEnv;
'config' => [
'_type' => AppType::MINI, // mp / mini / app
],
// Delegated payment scene (mp / mini / app / scan / h5)
'papay' => [
'_type' => PapayScene::MINI,
],
// Profit-sharing receivers
'receivers' => [
[
'type' => ReceiverType::MERCHANT_ID,
'account' => '1234567890',
],
],
// Virtual payment environment (0 = production-ish default, 1 = sandbox)
'env' => VirtualPayEnv::SANDBOX,
Unknown historical values retain previous behavior: unknown _type is treated as MP, unknown receiver type is passed through, and unknown env is treated as production.
use Yansongda\Pay\Enum\Alipay\ProductCode;
use Yansongda\Pay\Enum\Alipay\BizCode;
$payload = [
'biz_content' => [
'product_code' => ProductCode::FAST_INSTANT_TRADE_PAY,
// 'biz_code' => BizCode::FACE,
],
];
use Yansongda\Pay\Enum\Apple\SubscriptionStatus;
$status = SubscriptionStatus::ACTIVE; // or int 1-5
Pay::config([
'alipay' => [
'tenant1' => [
'app_id' => 'tenant1_app_id',
// ...
],
'tenant2' => [
'app_id' => 'tenant2_app_id',
// ...
],
],
]);
// Use tenant-specific configs
Pay::alipay('tenant1')->web([...]);
Listen to payment events (PSR-14) in EventServiceProvider:
protected $listen = [
\Yansongda\Pay\Event\AlipayEvent::ALIPAY_NOTIFY => [
\App\Listeners\HandleAlipayNotify::class,
],
\Yansongda\Pay\Event\WechatEvent::WECHAT_NOTIFY => [
\App\Listeners\HandleWechatNotify::class,
],
];
Extend for unsupported gateways in a service provider:
Pay::extend('paypal', function () {
return new \App\Pay\PaypalProvider();
});
Use this only for gateways the package does not support natively. For Apple Pay, use the built-in Pay::apple() provider instead.
Use queues for callback processing:
dispatch(new HandleAlipayNotify($data));
public function handle(HandleAlipayNotify $event)
{
$data = $event->data;
// Process order update, send notifications, etc.
}
This is especially useful for Apple App Store Server Notifications and WeChat callbacks that may trigger subscription checks or other slow work.
Enable logging in config/pay.php:
'logger' => [
'enable' => true,
'file' => storage_path('logs/pay.log'),
'level' => 'debug', // 'info' for production
],
public function handleWebhook()
{
$data = Pay::apple()->callback(); // JWS x5c verification + nested transaction JWS verification
// Process App Store Server Notification
}
For Alipay/WeChat, use the corresponding callback() method; it verifies signatures before returning data.
| Before | After |
|---|---|
Pay::MODE_SANDBOX |
PayMode::SANDBOX |
Pay::MODE_NORMAL |
PayMode::NORMAL |
Pay::MODE_SERVICE |
PayMode::SERVICE |
$config->getMode() === 1 |
$config->getMode()->isSandbox() |
$config->getMode() === Pay::MODE_SERVICE |
$config->getMode()->isService() |
$config->getModeEnum() |
$config->getMode() (returns PayMode) |
ProviderConfigInterface::getMode() now returns PayMode, not int. Integer comparisons will always evaluate to false, so audit any custom plugin code using === 0/1/2 before upgrading.AbstractConfig::getModeEnum() has been removed; $mode / setMode() / getMode() are now implemented once in AbstractConfig, so provider configs no longer redeclare them.mode values, so existing config/pay.php files keep working — only code comparisons break.| Issue | Solution |
|---|---|
| Signature verification fails | Ensure notify_url matches exactly (case-sensitive). Check cert paths and permissions. |
| Timeout errors | Increase http.timeout in config. Use async processing for heavy callbacks. |
| Multi-tenant conflicts | Explicitly specify tenant: Pay::alipay('tenant1')->web([...]). |
| WeChat cert errors | Use wechat_public_cert_path. For PHP-FPM, configure an array of certs keyed by certificate ID. |
| Refund failures | Verify out_refund_no is unique and out_trade_no exists. |
Apple cancel / close throws |
Apple has no equivalent server-side API. Use transaction/subscription queries and refund history instead. |
Apple refund is not an active refund |
Pay::apple()->refund() queries refund history only. Apple does not support server-side active refunds. |
| Apple callback rejected | Check bundle_id, environment, certificate validity, and that the notification JWS and the nested transaction JWS can both be verified. |
Apple merchantSession fails |
initiative_context is required. Ensure TLS mutual authentication is correctly configured through _http. |
| Apple RSA_v1 token decryption fails | RSA_v1 requires PHP >= 8.5 due to OAEP-SHA256 parameter constraints. EC_v1 has no PHP version requirement. |
Apple _status rejected |
Only SubscriptionStatus enum values or integers 1–5 are accepted; arbitrary ints fail the whitelist. |
src/Certificate/AppleRootCA-G3.pem and src/Certificate/AppleAAICAG3.pem. They are used by default when apple_root_ca / apple_intermediate_ca are not configured.payToken() performs PKCS#7 certificate-chain validation, ECDH key agreement, and AES-256-GCM decryption entirely locally — no new Composer dependencies are pulled in.issuer_id, bundle_id, api_key_id, and api_private_key.merchantSession() is a thin TLS-mutual-auth proxy; the certificate/key pair and TLS options go through the _http config block.PARAMS_APPLE_URL_MISSING (9234)PARAMS_APPLE_TOKEN_INVALID (9235)CONFIG_APPLE_INVALID (9413)DECRYPT_APPLE_FAILED (9612)Pay::config(['logger' => ['level' => 'debug']]);
$raw = Pay::alipay()->getLastRequest();
$raw = Pay::wechat()->getLastRequest();
use Yansongda\Pay\Enum\PayMode;
'mode' => PayMode::SANDBOX,
$config->getMode() is an enum instance; use ->value only for display, never for comparison.Certificates:
storage/app/certs/ and set appropriate permissions.'wechat_public_cert_path' => [
'CERT_ID_1' => 'path/to/cert1.pem',
'CERT_ID_2' => 'path/to/cert2.pem',
],
'apple_root_ca' => storage_path('certs/apple/AppleRootCA-G3.pem'),
'apple_intermediate_ca' => storage_path('certs/apple/AppleAAICAG3.pem'),
Amount Units:
10.00).1000 for ¥10.00).Notify URLs:
Enum compatibility:
toArray()['mode'] is a PayMode enum.toJson() / json_encode() still serialize mode as 0 / 1 / 2._type → MP, receiver type → pass-through, env → production), so partial migrations are safe.Pay::extend()).src/Crypto/Apple/ (Cryptor for general crypto utilities, TokenVerifier for token verify/decrypt, JwsVerifier for JWS verification), while AppleTrait remains the public facade for URL building, ES256 JWT, and crypto entry points. External calls to Pay::apple() are unchanged, so extending at Pay::apple() level is still the recommended integration point.How can I help you explore Laravel packages today?