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

Gateway Laravel Package

larabook/gateway

Larabook Gateway offers a clean Laravel abstraction for integrating and managing multiple gateways through a unified, driver-based API. It simplifies configuration, requests, responses, and extensibility so you can connect external services with consistent, testable code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require larabook/gateway
php artisan vendor:publish --provider="Larabook\Gateway\GatewayServiceProvider"

The published config/gateway.php is the single source of truth for the package — open it first. It defines a default driver plus a drivers map (ZarinPal, Mellat, Saman, Parsian, Pasargad, Saderat, Melli, Asan Pardakht, Pay.ir, IDPay, etc.). Each driver has its own credential keys (merchant_id, terminal_id, username/password, callback_url, sandbox). Populate them from .env, not hard-coded.

Second place to look: src/Contracts and src/Gateways.

The contract defines the lifecycle every driver must implement — typically request() / purchase(), pay() / redirect(), verify() / settle(). Because each Iranian PSP has a slightly different handshake, do not assume every driver exposes identical methods; read the driver you intend to ship with.

Fastest first use case: a ZarinPal sandbox purchase → callback → verify loop.

use Larabook\Gateway\Facades\Gateway;

$gateway = Gateway::driver('zarinpal');          // or omit for default
$gateway->setCallbackUrl(route('payment.callback', $order));
$gateway->purchase($order->amount, "Order #{$order->id}");
return $gateway->redirect();                     // renders/redirects to the bank

Then in the callback controller:

$gateway = Gateway::driver($order->gateway);
$gateway->setCallbackUrl(route('payment.callback', $order));

if (! $gateway->verify()) {          // or ->verify($amount)
    return redirect()->route('payment.failed');
}
$order->markPaid();
return redirect()->route('payment.success');

Get this single happy-path working in sandbox before touching production credentials — you cannot realistically test Iranian production gateways without a live merchant account.

Implementation Patterns

Wrap the package behind your own PaymentService. Do not call Gateway:: from controllers. One service class owns driver selection, callback URL generation, amount conversion, and verification, so you can swap drivers or fork the package without touching domain code.

final class PaymentService
{
    public function start(Order $order): RedirectResponse
    {
        return Gateway::driver($order->gateway)
            ->setCallbackUrl(route('payment.callback', ['order' => $order->uuid]))
            ->purchase($order->amount_rial, $order->description)
            ->redirect();
    }
}

Persist intent before redirecting. Create a payments row (uuid, gateway, amount_rial, authority/token, status, raw_request) before sending the user to the bank. The callback can arrive with nothing but a token in the query string — you need to resolve the order from that token, not from session.

Schema::create('payments', function (Blueprint $t) {
    $t->uuid('id')->primary();
    $t->foreignId('order_id');
    $t->string('gateway');
    $t->string('authority')->nullable()->index();
    $t->unsignedBigInteger('amount_rial');   // always integer rial, never float
    $t->enum('status', ['pending','paid','failed','canceled']);
    $t->json('request_payload')->nullable();
    $t->json('verify_payload')->nullable();
    $t->timestamps();
});

The authority column is your join key on the return trip: Payment::where('authority', $request->Authority)->firstOrFail().

Model callbacks as idempotent state transitions. Browsers retry, users double-click, and gateway webhooks can duplicate. Always:

DB::transaction(function () use ($payment, $request) {
    $payment = Payment::whereKey($payment->id)->lockForUpdate()->first();
    if ($payment->status === 'paid') return $payment;      // already handled
    if (! Gateway::driver($payment->gateway)->verify()) {
        $payment->update(['status' => 'failed']);
        return $payment;
    }
    $payment->update(['status' => 'paid']);
});

Multi-gateway routing. Treat the driver name as data (orders.gateway), not as code branching. Choose at checkout based on user's bank, fee, or an A/B flag, and store the choice on the payment row so the callback knows exactly which driver to verify against. Never verify with "the default gateway" — the returned token is only valid on the originating PSP.

Retry / reconciliation job. For status = 'pending' rows older than N minutes, run a scheduled job that re-checks with the gateway (many PSPs expose a verify or inquiry endpoint that tolerates repeated calls — confirm per driver) and marks abandoned payments. This is how you recover from "user closed the tab after paying."

Feature testing. Point the config at a fake/sandbox driver in phpunit.xml (GATEWAY_DRIVER=zarinpal, ZARINPAL_SANDBOX=true) and never hit a real PSP in CI. Assert on your own payments state machine, not on package internals. Optionally bind a stub implementation of the package contract into the container for pure unit tests of the service.

Local development: use expose, ngrok, or Laravel Valet's share to expose /payment/callback. Iranian gateways require a publicly resolvable HTTPS callback URL and will reject localhost. Put APP_URL in .env and derive the callback from route() so the same code works in staging and production.

Gotchas and Tips

The package is effectively unmaintained (last release Nov 2021). Before adopting, run composer require larabook/gateway on your target Laravel version. You will likely need "larabook/gateway": "*" with an alias, or a fork pinned via VCS, and may hit str_replace/curl deprecation warnings on PHP 8.1+. Lock it in your composer constraints and budget a fork.

Rial vs Toman is the single biggest source of "paid the wrong amount" bugs. Iranian PSPs consume rial in their APIs; users think in toman (÷10). Decide once: store amount_rial as an integer column, and convert only at the UI boundary and at the service call. Never use float or decimal for money here.

env() inside the package config breaks under config:cache. The package reads credentials via config('gateway.drivers.…'), which is fine — but if your .env values are consumed directly in a service or config that loads after caching, you will get null in production. Always php artisan config:clear after editing credentials, and prefer config() lookups in your own code.

Verify server-side, always, and verify the amount. A callback hitting your URL with ?Status=OK proves nothing. Only $gateway->verify() (which calls the PSP's server-to-server endpoint) is authoritative. Additionally, compare the returned amount to payments.amount_rial — some drivers return the paid sum and it may differ from what you requested.

Never trust the redirect alone — log raw payloads. Iranian gateway callback parameters vary in casing and encoding (Authority, authority, ResCode, Status, SaleReferenceId). Store $request->all() in verify_payload so you can debug a discrepancy from production logs without SSHing into the bank's portal.

Sandbox flags and URL pairs are per-driver. ZarinPal, Mellat, and Parsian each ship a separate sandbox host and sometimes different method names/return tokens. Don't assume sandbox: true alone switches the whole flow — check the driver's src/Gateways/<Name>*.php for the base URI logic.

Mellat/terminal-style gateways need more than a request/verify pair. Expect explicit settle() (or settleRequest) calls after verify, and a SaleReferenceId you must persist to refund or reconcile. If you skip settle, funds may appear "held" rather than captured.

Timeouts and SSL. Bank endpoints in Iran are frequently slow; set cURL timeouts in the 10–20s range and consider raising default_socket_timeout. Certificate validation failures usually mean a missing CA bundle on the server, not a package bug — verify with a plain curl against the PSP host before opening a package issue.

Extension point: implement the package contract for a missing PSP. Adding a new gate behind your PaymentService is just a class implementing the contract plus a config entry — cheaper than forking, and it keeps custom Iranian gateways out of your domain code.

Don't rely on the package for UI or receipts. It returns redirects and booleans; success/failure pages, SMS confirmations, invoice PDFs, and admin reporting are entirely yours to build.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core