ibrand/pay
Laravel payment package offering a unified, configurable API for integrating common payment gateways into PHP applications. It helps manage payment requests, gateway settings, and callback handling, reducing boilerplate for adding checkout and payment flows to Laravel projects.
The package is a thin Laravel wrapper around a WeChat/Alipay SDK (most likely yansongda/pay), so the fastest path is to understand what it wraps before writing any business code.
Install and inspect the dependency tree
composer require ibrand/pay
composer show -t ibrand/pay # see whether yansongda/pay, overtrue/wechat, etc. are underneath
The dependency tree tells you which SDK API surface is actually available — the wrapper usually forwards calls to it.
Publish and read the config. With little documentation, read the service provider to find the publish tag:
php artisan vendor:publish --provider="Ibrand\Pay\PayServiceProvider"
Open config/pay.php and map every key to an env var: app_id, mch_id, key, notify_url, return_url, and absolute cert paths for WeChat refunds/transfers.
First use case: a 0.01 scan/web payment. Register credentials in .env, then hit the facade from a tinker session or a throwaway route:
use Ibrand\Pay\Facades\Pay; // verify exact namespace in vendor/ibrand/pay/src
$order = [
'out_trade_no' => 'TEST' . time(),
'total_amount' => '0.01', // Alipay: yuan string
'subject' => 'Test order',
];
return Pay::alipay()->web($order); // returns a redirect/HTML response
The response is a Laravel Response — return it directly from the controller. If you get a signature error, you are almost certainly on a key/cert config problem, not a code problem.
Where to look first, in order: config/pay.php → the service provider → the facade class → the underlying SDK's own docs (which are far better than this package's).
Single payment service, not facades in controllers. Wrap the facade in one PaymentService so channel selection, order creation, and response formatting live in one place. Controllers should call $payments->create($order, 'alipay') and nothing else. This makes it trivial to swap or fork the package later.
Split create / notify / return into three routes.
POST /pay/{channel} — creates the order, redirects to the gateway.POST /pay/notify/{channel} — server-to-server callback. This is the only place you mark an order paid.GET /pay/return/{channel} — user-facing redirect. Use it for UX only; never trust it to flip order state.Callback handler skeleton (the part that matters):
public function notify(Request $request, string $channel)
{
$data = $request->all();
// 1. Verify signature via the SDK — do not hand-roll this.
// 2. Look up order by out_trade_no; 404 if missing.
// 3. Verify amount matches your stored amount (yuan vs fen — see gotchas).
// 4. Idempotency: if already paid, return success immediately.
// 5. Mark paid inside a DB transaction, then dispatch your fulfilment job.
return response('success'); // WeChat & Alipay both expect literal 'success'
}
Use a state machine on the order. pending → paid → refunding → refunded → closed. Callbacks are retried by both gateways for hours/days, so the idempotency guard on paid is not optional.
Refunds are a separate, manual flow. WeChat refunds require the cert files configured as absolute paths; Alipay refunds need out_request_no for partial/duplicate refund safety. Test refunds against the sandbox before shipping — they are the most common production incident with these gateways.
Per-tenant credentials. If you have multiple merchants, avoid mutating global config at runtime. Most wrappers accept a config array per call (Pay::alipay($config) or Pay::config($config)), which is safer than config()->set() mid-request.
pay/notify/* to VerifyCsrfToken::$except (or the middleware group equivalent). This is the #1 reason "the payment succeeded but the order stayed pending."success as plain text, not JSON. Both WeChat and Alipay retry until they see the literal string. Returning {"code":0} guarantees endless retries."0.01"), WeChat uses integer fen (1). Verify the amount in the callback against what you stored — never trust the gateway's amount alone.storage_path('certs/...') and store certs outside the repo.app_id mismatch, invalid sign, invalid total_fee all trace back to wrong key/cert/app_id pairs, or to a mismatch between the key type (RSA vs RSA2) and what's configured.composer.json, and read vendor/ibrand/pay/src before relying on anything undocumented. Budget for a fork: the wrapper is usually a few hundred lines, so forking to fix a namespace or add a channel is realistic.$request->all() on notify and the full gateway response on create/refund into a dedicated payments log channel. Signing bugs and amount mismatches are unreproducible without payloads.sandboxnew) are separate base URLs — confirm whether the package exposes a sandbox switch in config; if not, override the gateway URL at the SDK level.PaymentService is the seam that keeps that contained.How can I help you explore Laravel packages today?