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

Omnipay Alipay Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. The README gateway list — pick the Alipay product first, not the code.
  2. src/Aop*Gateway.php and src/Message/Aop* — exact setBizContent() keys and response accessors.
  3. 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.

Implementation Patterns

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.

Gotchas and Tips

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.

  • Copy the Alipay public key from the Alipay console, not your app public key.
  • Set RSA2 explicitly; older defaults may be RSA.
  • Store full PEM with headers; convert literal \n from env; don't trim or re-wrap per request.
  • PKCS#1 vs PKCS#8 mismatches throw OpenSSL coercion errors.
  • php artisan config:cache is a common breakage point for multiline keys. Test it.

Amounts and field names differ between product families.

  • AOP uses total_amount; Legacy uses total_fee. Both are yuan strings with two decimals.
  • Compute in integer cents, format at the edge, compare with bccomp. Never use floats.
  • out_trade_no must be unique and short enough (≤64 chars); subject ≤256 chars.
  • Page requires product_code = FAST_INSTANT_TRADE_PAY; App requires QUICK_MSECURITY_PAY. Wrong code yields invalid-parameter.
  • Currency is CNY only.

Notify endpoint rules.

  • Return exactly success (lowercase). JSON, HTML, or a 500 triggers retries.
  • Exclude the route from CSRF, auth, and session middleware.
  • WAIT_BUYER_PAY is a valid notification. Acknowledge it, but don't fulfill.
  • Verify app_id, seller_id, total_amount, and out_trade_no against your database. Signature verification alone doesn't prove order integrity.
  • Make fulfillment idempotent: 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.

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr