lokielse/omnipay-alipay
Omnipay Alipay driver for PHP: integrates Alipay payment gateways through the Omnipay library. Supports Express, Web, WAP, App, and mobile payments, plus refunds, order queries, and close operations. Works with Laravel and other PHP frameworks via Omnipay.
lokielse/omnipay-alipay is an Omnipay driver, not a Laravel package. Day to day you use it as a thin adapter: create a gateway, build a request with Alipay's biz_content, send it, then inspect the response. In Laravel, wrap it in a small service so controllers stay testable and you can swap/fork it later.
composer require lokielse/omnipay-alipay
composer show omnipay/omnipay # confirm whether you resolved v2 or v3
Look here first:
src/Aop*Gateway.php and src/Message/Aop* — exact setBizContent() keys and response accessors.src/Helper.php — signing/verification helpers if you need a custom callback endpoint.Minimal Laravel config:
// config/services.php
'alipay' => [
'app_id' => env('ALIPAY_APP_ID'),
'private_key' => str_replace('\n', "\n", env('ALIPAY_PRIVATE_KEY')),
'alipay_public_key' => str_replace('\n', "\n", env('ALIPAY_PUBLIC_KEY')),
'notify_url' => env('ALIPAY_NOTIFY_URL'),
'return_url' => env('ALIPAY_RETURN_URL'),
],
namespace App\Services;
use Omnipay\Omnipay;
class Alipay
{
public static function gateway(string $driver = 'AopPage')
{
$gateway = Omnipay::create("Alipay_{$driver}");
$gateway->setAppId(config('services.alipay.app_id'));
$gateway->setSignType('RSA2');
$gateway->setPrivateKey(config('services.alipay.private_key'));
$gateway->setAlipayPublicKey(config('services.alipay.alipay_public_key'));
$gateway->setReturnUrl(config('services.alipay.return_url'));
$gateway->setNotifyUrl(config('services.alipay.notify_url'));
return $gateway;
}
}
First real use case — PC checkout with AopPage:
$gateway = Alipay::gateway('AopPage');
$response = $gateway->purchase()
->setBizContent([
'subject' => $order->title,
'out_trade_no' => $order->no,
'total_amount' => bcdiv((string) $order->total_cents, '100', 2),
'product_code' => 'FAST_INSTANT_TRADE_PAY',
])
->send();
if (! $response->isSuccessful()) {
Log::error('alipay.purchase.failed', [
'data' => $response->getData(),
'message' => $response->getMessage(),
]);
}
$response->redirect();
On Omnipay 3, Omnipay::create() needs a PSR-18 client and PSR-7 request:
$gateway = Omnipay::create('Alipay_AopPage', $httpClient, $httpRequest);
// or: new \Omnipay\Alipay\AopPageGateway($httpClient, $httpRequest);
The gateway names follow the classes: Alipay_AopPage, Alipay_AopWap, Alipay_AopApp, Alipay_AopF2F, Alipay_LegacyExpress, Alipay_LegacyWap, Alipay_LegacyMobile, Alipay_LegacySecure, Alipay_LegacyBank.
Choose the gateway by payment surface, not by preference.
AopPage — desktop browser redirect. Typical Laravel checkout.AopWap — mobile browser redirect.AopApp — native app; returns a signed order string for the Alipay SDK.AopF2F — face-to-face / QR code; returns a qr_code string.Legacy* — older MD5 contracts. Different credentials (setPartner, setKey, setSellerId) and different field names (total_fee, not total_amount). Don't mix them with AOP configuration.Use setBizContent() as the primary interface. It maps almost 1:1 to Alipay's biz_content JSON. Use official Alipay API fields and let the gateway handle signing, encoding, and transport.
Notify is authoritative; return is only UX.
// VerifyCsrfToken::$except
protected $except = ['alipay/notify'];
public function notify(Request $request)
{
$response = Alipay::gateway('AopPage')
->completePurchase($request->all())
->send();
Log::info('alipay.notify', [
'payload' => $request->all(),
'ok' => $response->isSuccessful(),
]);
if (! $response->isSuccessful()) {
return response('fail');
}
$data = $response->getData();
if (($data['app_id'] ?? null) !== config('services.alipay.app_id')) {
return response('fail');
}
if (! in_array($data['trade_status'] ?? '', ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
return response('success'); // acknowledged, not fulfilled
}
DB::transaction(function () use ($data) {
$order = Order::where('no', $data['out_trade_no'])
->lockForUpdate()
->firstOrFail();
if ($order->paid_at) {
return;
}
abort_unless(
bccomp($order->amount, $data['total_amount'], 2) === 0,
422
);
$order->update([
'paid_at' => now(),
'trade_no' => $data['trade_no'],
]);
});
return response('success');
}
public function return(Request $request)
{
$response = Alipay::gateway('AopPage')
->completePurchase($request->all())
->send();
return $response->isSuccessful()
? redirect()->route('orders.show', $request->input('out_trade_no'))
: redirect()->route('checkout.failed');
}
Refunds, queries, and closes use the same setBizContent() shape.
$response = Alipay::gateway('AopPage')->refund()
->setBizContent([
'out_trade_no' => $order->no,
'refund_amount' => '0.01',
'out_request_no' => $refund->no, // mandatory for partial refunds
'refund_reason' => 'Customer request',
])
->send();
$data = Alipay::gateway('AopPage')->query()
->setBizContent(['out_trade_no' => $order->no])
->send()
->getData();
Alipay::gateway('AopPage')->close()
->setBizContent(['out_trade_no' => $order->no])
->send();
App payments return an order string; hand it to the SDK.
$orderString = Alipay::gateway('AopApp')
->purchase()
->setBizContent([
'subject' => $order->title,
'out_trade_no' => $order->no,
'total_amount' => bcdiv((string) $order->total_cents, '100', 2),
'product_code' => 'QUICK_MSECURITY_PAY',
])
->send()
->getOrderString(); // check the response class for the exact accessor
return response()->json(['order_string' => $orderString]);
QR payments return a code you render or hand to a POS.
$response = Alipay::gateway('AopF2F')
->purchase()
->setBizContent([
'out_trade_no' => $order->no,
'total_amount' => bcdiv((string) $order->total_cents, '100', 2),
'subject' => $order->title,
])
->send();
return response()->json(['qr_code' => $response->getQrCode()]);
Testing pattern. Do not call Alipay in tests. Bind a fake Alipay service or mock the gateway/response, then assert your controller handles isSuccessful() true/false, trade_status, duplicate notifications, and amount mismatches.
Maintenance risk is the main one. Last release was May 2020. It works, but Omnipay 3/PSR-18 wiring, PHP 8.x deprecations, and edge cases are on you. Keep it behind App\Services\Alipay so you can fork or replace it without touching controllers.
Key handling causes most "invalid signature" errors.
RSA2 explicitly; older defaults may be RSA.\n from env; don't trim or re-wrap per request.php artisan config:cache is a common breakage point for multiline keys. Test it.Amounts and field names differ between product families.
total_amount; Legacy uses total_fee. Both are yuan strings with two decimals.bccomp. Never use floats.out_trade_no must be unique and short enough (≤64 chars); subject ≤256 chars.product_code = FAST_INSTANT_TRADE_PAY; App requires QUICK_MSECURITY_PAY. Wrong code yields invalid-parameter.Notify endpoint rules.
success (lowercase). JSON, HTML, or a 500 triggers retries.WAIT_BUYER_PAY is a valid notification. Acknowledge it, but don't fulfill.app_id, seller_id, total_amount, and out_trade_no against your database. Signature verification alone doesn't prove order integrity.lockForUpdate(), paid_at check, unique index on trade_no.return_url is browser-controlled. Never mark paid there.notify_url must be publicly reachable; use ngrok/expose locally. Alipay retries for roughly 24 hours.Omnipay 3 specifics.
Omnipay::create() needs PSR-18/PSR-7. Install guzzlehttp/guzzle and php-http/guzzle7-adapter, or pass Http\Adapter\Guzzle7\Client and GuzzleHttp\Psr7\ServerRequest::fromGlobals().completePurchase() behavior around request data differs across v2/v3. Passing $request->all() explicitly is the most predictable.getMessage() is sometimes empty on failures. Always log getData() plus the raw response.Sandbox. AOP sandbox uses separate keys and a different host (openapi.alipaydev.com). Check the gateway class for a sandbox/endpoint switch; if absent, subclass and override the endpoint method for local testing. Legacy contract gateways generally have no sandbox.
Extension points. For an AOP API the package doesn't wrap, subclass the relevant gateway/message, or use setBizContent() if a wrapped method (purchase, refund, query, close) maps to the API you need. Override getEndpoint(), getDefaultParameters(), or a response accessor in a subclass kept under app/Omnipay.
Time handling. Alipay sends gmt_payment in GMT+8. Store UTC and convert at the edges; don't compare local strings directly.
How can I help you explore Laravel packages today?