ejoi/payment-gateway
Composer 安装命令:
composer require ejoi/payment-gateway
包简介
A modular, framework-agnostic PHP payment gateway abstraction with drivers for Malaysian and global providers (CHIP, Billplz, toyyibPay, Stripe, PayPal).
README 文档
README
A modular PHP payment-gateway abstraction for Malaysian and global providers. Write your checkout code once against a single interface; switch or add gateways with a config change and one small driver class.
Supported drivers: CHIP · Billplz · toyyibPay · Stripe · PayPal.
- 📖 docs/GATEWAYS.md — setup, usage & sandbox verification for every gateway.
- 🤝 handover.md — full project handover (architecture, decisions, testing, next steps).
- 🧩 examples/Laravel — a business-logic listener example.
Two layers
- Framework-agnostic core — pure PHP over a PSR-18 HTTP client. One
PaymentGatewayinterface, one driver per provider, normalized DTOs, onePaymentStatusenum. No framework, no database. - Laravel adapter (optional) — a
paymentsledger, an auto-registered webhook endpoint, an idempotent status flow, domain events, a reconciliation job, and out-of-the-box email notifications.
Every provider follows the same lifecycle; the differences (auth, amount unit, signature scheme, status vocabulary) are absorbed by each driver:
createPayment() → redirect to hosted page → verifyCallback() (signed webhook) → queryStatus() (requery to confirm)
Your application only ever deals with the PaymentGateway
interface, the DTOs (PaymentRequest, PaymentResponse, PaymentStatusResult), and the
PaymentStatus enum.
Requirements
- PHP 8.2+
- Any PSR-18 HTTP client (e.g. Guzzle) — auto-discovered.
Installation
composer require ejoi/payment-gateway guzzlehttp/guzzle
Laravel
The service provider and facades auto-discover. Publish config + migrations and migrate:
php artisan vendor:publish --tag=payment-gateway-config php artisan vendor:publish --tag=payment-gateway-migrations php artisan migrate
Set credentials in .env (see docs/GATEWAYS.md for each gateway's keys).
Every gateway defaults to sandbox until you set *_SANDBOX=false.
Quick start (Laravel)
use Ejoi\PaymentGateway\Data\{Customer, Money, PaymentRequest}; use Ejoi\PaymentGateway\Laravel\Facades\Payments; use Ejoi\PaymentGateway\Laravel\Events\PaymentStatusChanged; use Ejoi\PaymentGateway\Laravel\Jobs\ReconcilePendingPayments; use Ejoi\PaymentGateway\Enums\PaymentStatus; // 1. Create + persist a payment (swap 'billplz' for any gateway) $payment = Payments::create('billplz', new PaymentRequest( reference: $order->reference, amount: Money::fromMinor(4990, 'MYR'), description: "Order {$order->reference}", customer: new Customer($order->email, $order->name), redirectUrl: route('payment.return', $order), callbackUrl: route('payment-gateway.webhook', 'billplz'), )); return redirect()->away($payment->checkout_url); // 2. The webhook route is auto-registered and does verify → requery → persist → dedupe → event. // You just react to the outcome: Event::listen(PaymentStatusChanged::class, fn ($e) => $e->payment->status === PaymentStatus::Paid && $order->fulfil() ); // 3. Reconcile stragglers (FPX may not webhook) on a schedule: $schedule->job(ReconcilePendingPayments::class)->everyFiveMinutes()->withoutOverlapping(); // Optional: keep the webhook audit table bounded (90-day default retention). $schedule->command('model:prune', ['--model' => [\Ejoi\PaymentGateway\Laravel\Models\PaymentWebhook::class]])->daily();
On paid/failed the package also emails the merchant and customer (configurable). See
docs/GATEWAYS.md for per-gateway credentials and webhook registration.
Quick start (plain PHP core)
use Ejoi\PaymentGateway\PaymentGatewayManager; use Ejoi\PaymentGateway\Data\CallbackPayload; $manager = new PaymentGatewayManager($config); // $config = the array from config/payment-gateway.php $response = $manager->gateway('billplz')->createPayment($request); header('Location: ' . $response->redirectUrl); // On the webhook: $result = $manager->gateway('billplz')->verifyCallback(CallbackPayload::fromGlobals()); if (! $result->verified) { $result = $manager->gateway('billplz')->queryStatus($result->gatewayReference); } // $result->status is a normalized PaymentStatus — persist it yourself.
What the Laravel layer gives you
paymentsledger (Paymentmodel) — the package's own record, linked to your orders byreference.- Auto webhook route
POST /payment-gateway/webhook/{gateway}— verify → requery → persist → dedupe → fire event. CSRF-exempt; prefix/middleware configurable; disable withPAYMENT_GATEWAY_WEBHOOK_ROUTE=false. PaymentStatusChangedevent — fires once per real transition; listen for it to run business logic.ReconcilePendingPaymentsjob — requeries pending payments on your schedule.- Email notifications — merchant + customer, on
paid/failed, out of the box (Laravel Notifications). transaction_id— the provider's charge/transaction id, indexed. All other provider data stays inlast_response(JSON).
The two golden rules
- Trust the server webhook, never the browser return URL.
verifyCallback()returnsverified = falsewhen a signature can't be checked (e.g. toyyibPay) — then you mustqueryStatus(). - Status updates are idempotent. The webhook and the reconcile job can both land; the package only transitions a non-final payment, so a duplicate is a no-op.
Adding a new gateway
Extend AbstractGateway, implement three methods, register it — no fork needed:
use Ejoi\PaymentGateway\Gateway\AbstractGateway; final class MyGateway extends AbstractGateway { protected const NAME = 'mygateway'; public function createPayment(PaymentRequest $request): PaymentResponse { /* ... */ } public function verifyCallback(CallbackPayload $payload): PaymentStatusResult { /* ... */ } public function queryStatus(string $gatewayReference): PaymentStatusResult { /* ... */ } } $manager->extend('mygateway', fn ($config, $http) => new MyGateway($config, $http));
Add a config block under gateways and you're done. Mirror
BillplzGateway — it's the reference implementation.
Architecture
src/
├── Contracts/ PaymentGateway (the interface), HttpClient
├── Data/ PaymentRequest, PaymentResponse, PaymentStatusResult,
│ CallbackPayload, Customer, Money (immutable DTOs)
├── Enums/ PaymentStatus, PaymentMethod, Currency
├── Config/ GatewayConfig Http/ PsrHttpClient, HttpResponse
├── Support/ Signature (constant-time HMAC) Exceptions/ (typed hierarchy)
├── Gateway/
│ ├── AbstractGateway.php
│ └── Drivers/ Chip · Billplz · Toyyibpay · Stripe · Paypal
├── Laravel/ ServiceProvider · Facades · Payments · Models (Payment, PaymentWebhook)
│ Http/PaymentWebhookController · Events · Listeners · Jobs · CallbackPayloadFactory
└── PaymentGatewayManager.php (resolves drivers by name from config)
config/ · database/migrations/ · tests/ · docs/GATEWAYS.md · handover.md
Provider notes
| Gateway | Amount unit | Callback signature | Notes |
|---|---|---|---|
| CHIP | minor (sen) | RSA public-key signature | Cross-border & crypto capable |
| Billplz | minor (sen) | HMAC-SHA256 x_signature |
MYR only; webhook mandatory |
| toyyibPay | minor (sen) | none → always requery | MYR only; cheapest FPX |
| Stripe | minor (cents) | Stripe-Signature (HMAC + 300s window) |
Hosted Checkout Session |
| PayPal | decimal string | verify-webhook-signature API | Orders v2 (capture on requery) |
Testing
composer install php vendor/bin/phpunit
The suite covers all five drivers (with signature verification), Money, the manager, and the
Laravel persistence/notification flow (via orchestra/testbench on in-memory SQLite). Drivers are
unit-tested with a FakeHttpClient (no network). See handover.md for details.
License
MIT.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-11