Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pay Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. 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.

  2. 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\*).
  3. 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.

  4. 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());
    }
    
  5. 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();
        }
    }
    

Implementation Patterns

1. Gateway-Specific Workflows

Alipay (Web/APP/Scan)

// 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',
]);

WeChat (Mini Program/JSAPI)

// 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,
]);

Apple Pay (new in v3.8)

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.

Unified Query/Refund

// 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
]);

2. Enums in v3.8+

v3.8 introduces enums across providers. Default behavior and wire payloads remain byte-compatible unless you rely on the removed mode constants or getModeEnum().

PayMode

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.

WeChat Enums

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.

Alipay Enums

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,
    ],
];

Apple Subscription Status

use Yansongda\Pay\Enum\Apple\SubscriptionStatus;

$status = SubscriptionStatus::ACTIVE; // or int 1-5

3. Multi-Tenant & Dynamic Configs

Pay::config([
    'alipay' => [
        'tenant1' => [
            'app_id' => 'tenant1_app_id',
            // ...
        ],
        'tenant2' => [
            'app_id' => 'tenant2_app_id',
            // ...
        ],
    ],
]);

// Use tenant-specific configs
Pay::alipay('tenant1')->web([...]);

4. Event-Driven Extensions

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,
    ],
];

5. Custom Provider Integration

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.


6. Asynchronous Processing

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.


7. Logging & Debugging

Enable logging in config/pay.php:

'logger' => [
    'enable' => true,
    'file' => storage_path('logs/pay.log'),
    'level' => 'debug', // 'info' for production
],

8. Webhook Validation

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.


Gotchas and Tips

1. Breaking Changes in v3.8

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.
  • Configuration still accepts historical integer mode values, so existing config/pay.php files keep working — only code comparisons break.

2. Common Pitfalls

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.

3. Apple Pay Specifics

  • Built-in Apple public certificates are bundled at 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.
  • App Store Server API authentication uses an App Store Connect ES256 JWT built from issuer_id, bundle_id, api_key_id, and api_private_key.
  • Callback verification checks bundle ID and environment against configuration, plus certificate validity, aligned with Apple's official library behavior. Both the outer notification JWS and the nested transaction JWS are verified.
  • merchantSession() is a thin TLS-mutual-auth proxy; the certificate/key pair and TLS options go through the _http config block.
  • Apple exception codes to watch for:
    • PARAMS_APPLE_URL_MISSING (9234)
    • PARAMS_APPLE_TOKEN_INVALID (9235)
    • CONFIG_APPLE_INVALID (9413)
    • DECRYPT_APPLE_FAILED (9612)

4. Debugging Tips

  • Enable debug logs:
    Pay::config(['logger' => ['level' => 'debug']]);
    
  • Inspect raw requests:
    $raw = Pay::alipay()->getLastRequest();
    $raw = Pay::wechat()->getLastRequest();
    
  • Use sandbox mode with the enum:
    use Yansongda\Pay\Enum\PayMode;
    
    'mode' => PayMode::SANDBOX,
    
  • Dump the resolved mode: $config->getMode() is an enum instance; use ->value only for display, never for comparison.

5. Configuration Quirks

  • Certificates:

    • Store certs under storage/app/certs/ and set appropriate permissions.
    • For WeChat, use an array of cert paths if multiple certificates are provided:
      'wechat_public_cert_path' => [
          'CERT_ID_1' => 'path/to/cert1.pem',
          'CERT_ID_2' => 'path/to/cert2.pem',
      ],
      
    • For Apple, override bundled certificates only when necessary:
      'apple_root_ca' => storage_path('certs/apple/AppleRootCA-G3.pem'),
      'apple_intermediate_ca' => storage_path('certs/apple/AppleAAICAG3.pem'),
      
  • Amount Units:

    • Alipay: amount in CNY (e.g. 10.00).
    • WeChat: amount in fen (e.g. 1000 for ¥10.00).
    • Apple: amounts are tied to App Store products/transactions rather than the same direct amount fields used by Alipay/WeChat.
  • Notify URLs:

    • Must be HTTPS and publicly accessible.
    • Test locally with tools such as ngrok.
  • Enum compatibility:

    • toArray()['mode'] is a PayMode enum.
    • toJson() / json_encode() still serialize mode as 0 / 1 / 2.
    • Unknown WeChat historical enum values retain old fallback behavior (_type → MP, receiver type → pass-through, env → production), so partial migrations are safe.

6. Extension Points

  • Custom gateways/providers can be registered through the package's extension mechanism (Pay::extend()).
  • Events can be used to decouple notification handling from controllers.
  • Queues are recommended for Apple/WeChat/Alipay callback processing that involves subscription checks, order updates, or external API calls.
  • v3.8 moves Apple cryptography internally into 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
adachsoft/changelog-linter