overtrue/laravel-payment
A Laravel payment integration package that provides a unified API for Alipay and WeChat Pay. It simplifies configuring credentials, creating payments, handling callbacks, and supporting common scenarios like web, app, and QR payments—ideal for adding Chinese payment gateways to Laravel apps.
This release (4.0.0) is a compatibility refresh, not a feature release. The day-to-day API is unchanged, but the platform floor moved: PHP ^8.3 and Laravel ^13.30 only. If you are on Laravel 12 or PHP 8.2, stay on 3.x until you upgrade.
overtrue/laravel-payment:^4.0 → gateway drivers. The package now requires league/omnipay ^3.2.1 and omnipay/common ^3.5.1, so any separately installed driver must be compatible with those. Check with composer why-not league/omnipay 3.2.1 and by running your own gateway tests, not just composer update.config/payment.php is the map of the package: default gateway, gateways.*.driver, and gateways.*.options. Most day-to-day confusion is a config/env mismatch, not code.use Overtrue\LaravelPayment\Facades\Payment;
$response = Payment::gateway('alipay')->purchase([
'out_trade_no' => $order->number,
'total_amount' => $order->total,
'subject' => $order->description,
'notify_url' => route('payments.notify', ['gateway' => 'alipay']),
'return_url' => route('payments.return'),
]);
return $response->isRedirect()
? redirect($response->getRedirectUrl())
: back()->withErrors($response->getMessage());
The returned object is an Omnipay response. The methods you will use daily are isRedirect(), getRedirectUrl(), isSuccessful(), getTransactionReference(), and getMessage().
completePurchase() on the same gateway with the incoming request data, verify the result, then fulfil the order idempotently.Explicit gateway per transaction. Use Payment::gateway() with no argument for the configured default, and an explicit name when the choice is business-driven (currency, region, tenant, payment method). Because gateway names remain nullable, passing null deliberately falls back to the default — make that decision explicit in your code instead of letting a null tenant setting silently route traffic.
$gateway = Payment::gateway($order->payment_method); // 'alipay', 'wechat', ...
Config-driven multi-gateway setup. Keep credentials in config/payment.php reading from env, one entry per gateway name. This lets you add a gateway without touching application code and lets ops switch the default per environment. Run php artisan config:cache as part of deploy and remember cached config ignores later .env edits.
Callback verification pattern. A robust notify handler looks like:
public function notify(Request $request, string $gateway)
{
$response = Payment::gateway($gateway)->completePurchase($request->all());
abort_unless($response->isSuccessful(), 400, $response->getMessage());
// Verify amount, currency and out_trade_no against your order before fulfilling.
// Enforce a unique constraint on (gateway, transaction_reference) for idempotency.
}
Do the same in the return handler, but treat it as display-only: it can be spoofed or abandoned. Fulfilment belongs in the notify path.
Refunds and queries reuse the same gateway. Payment::gateway('alipay')->refund([...]) and ->query([...]) follow the purchase shape. Pass the original out_trade_no and your own out_refund_no; persist both so retries are idempotent.
Gateway caching and state. Gateway instances are cached, and 4.0 explicitly preserves that behaviour. Treat the instance as shared and stateless: pass all per-transaction parameters to each purchase() / refund() call rather than mutating the gateway with setters. If a driver requires per-request configuration, resolve a distinct configured gateway name rather than mutating the cached one.
Custom gateways. If you maintain an in-house Omnipay driver, point the config driver at your class (or register it through the package's extension point if your version exposes one) and cover it with your own integration test. The release notes confirm the factory implementation is unchanged, so existing custom drivers should continue to resolve — the risk is their Omnipay dependency constraints, not the factory.
Testing. Swap the facade root or bind a fake gateway in the container; the package's own suite uses a dummy gateway and makes no live calls. Assert the outgoing parameter array (order number, amount, notify URL) and drive callback tests with captured fixture payloads from each provider. Add one test that resolves every configured gateway so a bad driver or missing credential fails CI, not production.
composer.lock on 13.29 will fail resolution. Upgrade Laravel first, then the package.composer update proves nothing about signature compatibility.Payment::gateway(null) resolves the default. This is convenient and dangerous: a null column from the database will silently route to the default gateway. Validate the payment method before it reaches the factory.error_reporting masks, and run CI on both lowest and latest dependency sets.InvalidRequestException for missing or malformed parameters, or driver-level signature errors. Log the outgoing parameter array and the response body with secrets redacted; never log private keys, API secrets or full callback payloads containing customer data.isSuccessful() alone does not make a handler safe to run twice.php artisan config:clear locally and redeploy with config:cache. A cached config with old keys is the most common "it works locally" failure for this package.How can I help you explore Laravel packages today?