fd6130/billplz-bundle
A Laravel bundle for integrating the Billplz payment gateway into your PHP applications. Simplifies creating bills, handling payment callbacks, and verifying transactions.
This release (v1.0.3) is a small patch: it fixes an optional parameter deprecation notice — the kind PHP 8.0+ emits when a parameter with a default value is declared before a required one. If you're on PHP 8.1/8.2/8.3 and were seeing Deprecated: Optional parameter $x declared before required parameter $y in your logs on v1.0.2, upgrading silences it. There are no API, config, or signature changes, so it's a drop-in bump.
Minimal steps:
composer require fd6130/billplz-bundle (or composer update fd6130/billplz-bundle if already installed — anything below 1.0.3 will spam deprecations on modern PHP).config/packages/billplz.yaml in Symfony; in Laravel you'll be wiring the service yourself — see below) and set your API key, x-signature key, and the sandbox vs. production base URL.createBill, getBill, deleteBill, and collection methods live) and the Billplz v3 API docs for the exact payload shape. The bundle is a thin wrapper; the authoritative contract is Billplz's, not the bundle's.url → handle the redirect back with getBill. Only after that works should you wire the webhook.Note on framework fit: despite the "Laravel developer" framing, this is a Symfony bundle, not a Laravel package. Expect no Billplz facade, no php artisan vendor:publish, no config file in config/. You'll register the bundle (Symfony) or bind the client class into Laravel's container with a small service provider of your own.
The three-leg payment flow. Billplz is redirect-based, and every integration has three legs that must all be handled:
// 1. Create — server side, before redirect
$bill = $billplz->createBill($collectionId, [
'email' => $order->email,
'name' => $order->name,
'amount' => $order->total_cents, // integer, cents
'callback_url' => route('billplz.webhook'),
'redirect_url' => route('billplz.return'),
'reference_1' => $order->id, // your idempotency anchor
]);
$order->update(['billplz_bill_id' => $bill['id']]);
return redirect($bill['url']);
// 2. Return — user-facing, cosmetic only
$bill = $billplz->getBill(request('billplz')['id']);
// show "paid / pending / failed" — do NOT fulfil here
// 3. Webhook — authoritative, fulfils the order
Wrap the client in your own service. Don't inject the bundle's client into controllers. A thin BillplzService gives you one place to swap base URLs for sandbox, add logging, and map the bundle's return types to your domain. In Laravel, bind it as a singleton in a BillplzServiceProvider and read credentials from config/services.php / .env.
Always verify x_signature on the webhook. This is the single most-skipped step. The signature is an HMAC-SHA256 over the callback parameters, sorted by key, joined as key + value concatenated, excluding x_signature itself:
$payload = collect($request->all())->except('x_signature')->sortKeys();
$source = $payload->map(fn ($v, $k) => $k.$v)->implode('');
$valid = hash_equals(hash_hmac('sha256', $source, config('services.billplz.x_signature')), $request->x_signature);
abort_unless($valid, 403);
Use hash_equals, not ===.
Treat the webhook as idempotent and out-of-order. Billplz will retry on non-2xx, and paid events can arrive twice. Key your handler on billplz_bill_id + paid_at/state, make fulfilment a no-op if already processed, and return 200 fast — push real work to a queued job.
Reconcile with getBill as a backstop. For orders stuck in pending past a timeout, a scheduled command that polls getBill for open bills catches webhooks you never received. Same code path as the webhook handler, so keep the state transition in one method.
Sandbox and production are separate worlds. Different base URL, different API key, different x-signature key, different collection ID. Model this as two config blocks selected by an env var — not a single key you swap manually before deploying.
^1.0.3 so a fresh install doesn't resolve back to the noisy version.float arithmetic anywhere near the amount. round($total * 100) is fine; $total * 100 is not. Billplz does not do multi-currency.getBill poll) is the source of truth.localhost or *.test. Use a tunnel (ngrok/expose) in local dev, and remember HTTPS in production.createBill that returns null instead of throwing will silently produce a broken checkout.app(BillplzClient::class) scattered through controllers.Http client logging and exception reporting will happily dump the Authorization header. Exclude them explicitly.How can I help you explore Laravel packages today?