PHPackages                             ejoi/payment-gateway - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. ejoi/payment-gateway

ActiveLibrary[Payment Processing](/categories/payments)

ejoi/payment-gateway
====================

A modular, framework-agnostic PHP payment gateway abstraction with drivers for Malaysian and global providers (CHIP, Billplz, toyyibPay, Stripe, PayPal).

v1.0.0(1mo ago)063↓25%MITPHPPHP ^8.2

Since Jul 7Pushed 1mo agoCompare

[ Source](https://github.com/ejoi8/payment-gateway)[ Packagist](https://packagist.org/packages/ejoi/payment-gateway)[ RSS](/packages/ejoi-payment-gateway/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (8)Versions (2)Used By (0)

ejoi/payment-gateway
====================

[](#ejoipayment-gateway)

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](docs/GATEWAYS.md)** — setup, usage &amp; sandbox verification for every gateway.
- 🤝 **[handover.md](handover.md)** — full project handover (architecture, decisions, testing, next steps).
- 🧩 **[examples/Laravel](examples/Laravel/README.md)** — a business-logic listener example.

---

Two layers
----------

[](#two-layers)

1. **Framework-agnostic core** — pure PHP over a PSR-18 HTTP client. One `PaymentGateway` interface, one driver per provider, normalized DTOs, one `PaymentStatus` enum. No framework, no database.
2. **Laravel adapter (optional)** — a `payments` ledger, 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`](src/Contracts/PaymentGateway.php)interface, the DTOs (`PaymentRequest`, `PaymentResponse`, `PaymentStatusResult`), and the [`PaymentStatus`](src/Enums/PaymentStatus.php) enum.

---

Requirements
------------

[](#requirements)

- PHP 8.2+
- Any [PSR-18 HTTP client](https://www.php-fig.org/psr/psr-18/) (e.g. Guzzle) — auto-discovered.

Installation
------------

[](#installation)

```
composer require ejoi/payment-gateway guzzlehttp/guzzle
```

### Laravel

[](#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](docs/GATEWAYS.md)** for each gateway's keys). Every gateway defaults to **sandbox** until you set `*_SANDBOX=false`.

---

Quick start (Laravel)
---------------------

[](#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](docs/GATEWAYS.md) for per-gateway credentials and webhook registration.

Quick start (plain PHP core)
----------------------------

[](#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
--------------------------------

[](#what-the-laravel-layer-gives-you)

- **`payments` ledger** (`Payment` model) — the package's own record, linked to your orders by `reference`.
- **Auto webhook route** `POST /payment-gateway/webhook/{gateway}` — verify → requery → persist → **dedupe** → fire event. CSRF-exempt; prefix/middleware configurable; disable with `PAYMENT_GATEWAY_WEBHOOK_ROUTE=false`.
- **`PaymentStatusChanged` event** — fires once per real transition; listen for it to run business logic.
- **`ReconcilePendingPayments` job** — 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 in `last_response` (JSON).

The two golden rules
--------------------

[](#the-two-golden-rules)

1. **Trust the server webhook, never the browser return URL.** `verifyCallback()` returns `verified = false` when a signature can't be checked (e.g. toyyibPay) — then you **must** `queryStatus()`.
2. **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
--------------------

[](#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`](src/Gateway/Drivers/BillplzGateway.php) — it's the reference implementation.

---

Architecture
------------

[](#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
--------------

[](#provider-notes)

GatewayAmount unitCallback signatureNotesCHIPminor (sen)RSA public-key signatureCross-border &amp; crypto capableBillplzminor (sen)HMAC-SHA256 `x_signature`MYR only; webhook mandatorytoyyibPayminor (sen)**none** → always requeryMYR only; cheapest FPXStripeminor (cents)`Stripe-Signature` (HMAC + 300s window)Hosted Checkout SessionPayPal**decimal string**verify-webhook-signature APIOrders v2 (capture on requery)Testing
-------

[](#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](handover.md) for details.

License
-------

[](#license)

MIT.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance89

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

54d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/94e81736678b0648703653f1c45eda0dc6c79dc8a0871aa90b4f01cbb43271d3?d=identicon)[ejoi8](/maintainers/ejoi8)

---

Top Contributors

[![fadzli-zulkefli](https://avatars.githubusercontent.com/u/30220408?v=4)](https://github.com/fadzli-zulkefli "fadzli-zulkefli (3 commits)")

---

Tags

stripepaymentgatewaypaypalMalaysiafpxchipbillplztoyyibpay

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ejoi-payment-gateway/health.svg)

```
[![Health](https://phpackages.com/badges/ejoi-payment-gateway/health.svg)](https://phpackages.com/packages/ejoi-payment-gateway)
```

###  Alternatives

[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86538.6k](/packages/flow-php-flow)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

36863.5k2](/packages/telnyx-telnyx-php)[tempest/framework

The PHP framework that gets out of your way.

2.3k42.4k21](/packages/tempest-framework)[shopware/app-php-sdk

Shopware App SDK for PHP

15130.8k3](/packages/shopware-app-php-sdk)[n1ebieski/ksef-php-client

PHP API client that allows you to interact with the API Krajowego Systemu e-Faktur

9197.7k](/packages/n1ebieski-ksef-php-client)[cakephp/cakephp

The CakePHP framework

8.9k20.4M1.9k](/packages/cakephp-cakephp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
