parsisolution/gateway
Parsisolution Gateway is a Laravel package offering a unified interface for multiple payment gateways. It simplifies configuration, payment initiation, and callback verification, letting PHP applications integrate online payments through a consistent, reusable API.
Install the package
composer require parsisolution/gateway
Laravel’s package discovery registers the service provider automatically. If you’re on an older Laravel version, add the provider and any facade alias manually in config/app.php.
Publish the configuration
php artisan vendor:publish --provider="Parsisolution\Gateway\GatewayServiceProvider"
Check the generated config/gateway.php (name may differ slightly) first — it is the source of truth for:
Set environment variables
GATEWAY_DRIVER=zarinpal
GATEWAY_MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
GATEWAY_CALLBACK_URL="${APP_URL}/payment/callback"
GATEWAY_SANDBOX=true
Keep the sandbox on until you have a full round-trip working against the gateway’s test environment.
Where to look first
zarinpal vs zarinpal_rest).config/gateway.php for the credential shape each driver expects.src/Drivers/ directory to confirm which methods each driver actually implements (some support refund/inquiry, many do not).First use case — a single purchase round-trip
// 1. Create a payment and get a redirect URL
$payment = Gateway::driver('zarinpal')->request($amountRial, $description);
return redirect($payment->getRedirectUrl());
// 2. In the callback route, verify using the gateway’s reference
$result = Gateway::driver('zarinpal')->verify($request->input('Authority'), $amountRial);
Get this working end-to-end in sandbox before adding a second driver.
Wrap the package in your own service. Don’t call the package facade directly from controllers. Introduce an App\Services\PaymentService (or PaymentGateway interface) that maps your domain’s Order to the package’s request/verify calls. This gives you one place to swap drivers, add logging, and mock in tests.
Two-phase flow is mandatory for Iranian gateways. Almost every gateway follows request → redirect → callback → verify:
// Phase 1: persist before redirecting
$tx = PaymentTransaction::create([
'order_id' => $order->id,
'gateway' => $driver,
'amount' => $order->total, // store the exact unit sent to gateway
'status' => 'pending',
'authority'=> null,
]);
$payment = Gateway::driver($driver)->request($order->total, $order->description);
$tx->update(['authority' => $payment->getAuthority()]);
return redirect($payment->getRedirectUrl());
// Phase 2: verify in callback
$tx = PaymentTransaction::where('authority', $request->input('Authority'))->firstOrFail();
$result = Gateway::driver($tx->gateway)->verify($tx->authority, $tx->amount);
if ($result->isSuccessful()) {
$tx->update(['status' => 'paid', 'ref_id' => $result->getReferenceId()]);
$order->markPaid();
}
Store the gateway key on the transaction, not just the config. A user may start with Zarinpal and you may later switch the default driver to Zibal. The callback must verify with the same driver that created the request.
Use a transaction state machine. States like pending → redirected → verified | failed | cancelled make reconciliation and admin debugging far easier than a single paid boolean. Iranian gateways frequently deliver a callback but the user closes the browser before the redirect; you need pending payments you can reconcile later.
Multi-gateway selection per order. Let the user pick a gateway on checkout, then pass the driver explicitly:
Gateway::driver($order->preferred_gateway)->request(...);
Persist the choice. Keep a fallback driver if the primary gateway is down.
Callback route hygiene. Register a single callback route that dispatches on the stored transaction, not on a query-string gateway parameter:
Route::match(['get','post'], '/payment/callback', PaymentCallbackController::class)
->name('payment.callback');
Use match because different gateways return GET vs POST.
Testing strategy. Wrap the package behind your PaymentGateway interface and bind a fake in tests that returns a canned success/failure. Reserve real package calls for an integration test suite hitting the sandbox. Never let CI hit production gateways.
Logging pattern. Log the raw request and response payloads (minus secrets) around every request() and verify() call. Iranian gateway error messages are often opaque Persian strings; you’ll need the raw response when reconciling.
$request->input('amount'). Attackers can tamper with callback parameters.paid, return success without calling the gateway again.Authority, IDPay sends id + order_id, Pay.ir sends trans_id + factorNumber. Normalize immediately in one controller and look up your transaction by the field that driver uses.GATEWAY_SANDBOX=true may not affect every driver; check each driver’s config block before assuming test mode is active.src/Drivers/ before building an admin “refund” button. If unsupported, the driver may throw a BadMethodCallException or NotSupportedException.cURL error 60, install ca-certificates in the container; do not disable verification in production.php artisan config:clear. Stale config/gateway.php values are a frequent cause of “wrong merchant” errors.GATEWAY_CALLBACK_URL will redirect to a blank gateway error page.request() — you may create duplicate payment authorities. Retrying verify() is safe only if idempotent on your side.Driver or contract in src/) and register it under a custom key in config/gateway.php. This is usually cleaner than forking the package.composer.json and review diffs before upgrading — gateway APIs change without much notice, and breaking changes may not be well documented.pending transactions older than N minutes and calls the gateway’s inquiry (where supported) or manually flags them for review. Real customers lose connections; a payments table with only paid/unpaid will leak money.How can I help you explore Laravel packages today?