php-monsters/shaparak
PHP package for integrating Iran's Shaparak payment gateway into Laravel or standalone PHP apps. It handles payment requests, gateway redirects, and transaction verification, making it simple to accept and confirm online payments with a clean, configurable setup.
The package is a gateway handler for Iranian IPG (Internet Payment Gateway) providers — the local equivalent of Stripe/PayPal wrappers. Install via Composer:
composer install php-monsters/shaparak
Most packages of this shape ship a service provider with auto-discovery, so nothing is needed in config/app.php on Laravel 9+. Publish the config first — this is the file you will live in:
php artisan vendor:publish --provider="PhpMonsters\Shaparak\ShaparakServiceProvider"
That should drop a config/shaparak.php containing driver entries (one per gateway — Zarinpal, IDPay, Pay.ir, Zibal, etc.), each with API keys, terminal IDs, and a callback URL pointing at a route in your app.
Your first real use case is the standard two-leg payment flow:
$payment = Shaparak::driver('zarinpal')
->amount(50000) // verify units: rials vs. tomans!
->callback(route('payment.callback'))
->request();
return redirect($payment->redirectUrl());
Look first at the config file, then at whatever the Shaparak facade resolves to (usually a Manager extending Illuminate\Support\Manager). That manager tells you exactly which driver names are valid. The callbacks in the package's docblocks/README are usually self-sufficient minimal examples — copy them before wiring anything into your own domain model.
Driver-per-gateway via Laravel's Manager. Expect Shaparak::driver('name') to behave like Cache::store(): shared driver config, per-call fluent options. Treat the fluent builder as immutable-ish per request — create a fresh payment per transaction rather than reusing a builder.
Separate "request" and "verify" into two methods on your app. The cleanest integration is a PaymentService in your domain that owns order state, and calls the package only for the wire protocol:
class PaymentService
{
public function start(Order $order, string $gateway): RedirectResponse
{
$result = Shaparak::driver($gateway)
->amount($order->total_rial)
->orderId($order->id)
->callback(route('payment.callback', ['order' => $order->id]))
->request();
$order->update(['authority' => $result->authority(), 'gateway' => $gateway]);
return redirect($result->redirectUrl());
}
public function verify(Order $order, Request $request): bool
{
$result = Shaparak::driver($order->gateway)
->amount($order->total_rial)
->authority($request->input('Authority') ?? $order->authority)
->verify();
return $result->isSuccessful();
}
}
One callback route, dispatched by gateway. Gateways return different query param names (Authority, authority, trackId, token). Route through a controller that looks up the order, resolves the driver from the order record, and reads params defensively (the driver should normalize, but you will hit inconsistencies).
Multiple gateways as fallbacks. A common production pattern: try the user's chosen gateway; on a GatewayException before redirect, fail over to a second driver. Do this only on the request leg — never fail over on the verify leg or you double-charge.
Testing. Wrap the package behind your PaymentService interface and fake that — most Shaparak packages don't ship a built-in fake() like Cashier does. If they do expose an Http::fake seam internally, prefer faking at the HTTP layer; the drivers hit well-known endpoints.
$request->amount from the callback — use your stored order total.payments table row in a verifying state and short-circuit repeat calls; most gateways return "already verified" errors that you must treat as success if the amount/authority match.verify() is that you send authority + amount to the gateway and get a signed confirmation back. If your code says if ($request->Status === 'OK') you have a vulnerability. Always call verify.config(['shaparak.drivers.x.merchant_id' => ...]) per-tenant, php artisan config:cache in production will silently ignore it. Pass credentials through the driver builder instead, or bind a per-tenant config.request() and verify() in try/catch for connection exceptions and surface a friendly "try another gateway" message. Default Guzzle timeout is often too aggressive at 5s — bump it in whatever HTTP client config the package exposes.sandbox boolean per driver; there's no unified switch.SoapClient internals, remember Http::fake won't intercept them — mock at your service boundary instead.AbstractDriver or similar) and register it under shaparak.drivers in config. This is the intended extension seam — do not fork.referenceId/rrn from a failed verification, and by then you'll have lost the callback request context.How can I help you explore Laravel packages today?