gabrielextso/paybox-bundle
Symfony bundle for integrating the Paybox payment gateway into PHP applications. It provides configuration and service wiring to initiate Paybox payments and handle payment responses, simplifying secure online payment setup in Symfony projects.
The first thing to internalize: there is no daily leverage for a Laravel developer here. gabrielextso/paybox-bundle is a fork of lexik/paybox-bundle, a Symfony bundle. It ships a Bundle class, a DI extension with a Configuration tree, YAML/XML service definitions, and Symfony-idiomatic classes under the Lexik\Bundle\PayboxBundle namespace. There is no ServiceProvider, no facade, no publishable config, no routes/ file. composer require will resolve, but nothing boots.
So "getting started" splits into two tracks, and you should pick one deliberately.
Track A — you are actually on Symfony (rare in a Laravel-tilted codebase, but possible for a legacy service):
composer require gabrielextso/paybox-bundle
Register Lexik\Bundle\PayboxBundle\LexikPayboxBundle in config/bundles.php, then configure the contract:
# config/packages/lexik_paybox.yaml
lexik_paybox:
parameters:
production: '%env(bool:PAYBOX_PRODUCTION)%'
site: '1234567' # PBX_SITE
rank: '99' # PBX_RANG
identifier: '123456789' # PBX_IDENTIFIANT
key: '%env(PAYBOX_HMAC_KEY)%'
Read, in this order:
Lexik\Bundle\PayboxBundle\Paybox\Paybox — request builder and signing.Lexik\Bundle\PayboxBundle\Paybox\Response — IPN signature verification and return parsing.Resources/config/services.xml — confirm current service IDs before injecting anything. Service names drift across forks; do not copy service IDs from a blog post.lexik/LexikPayboxBundle README. It is the only real documentation. Diff the fork against it on day one to see what was actually changed.First use case — checkout redirect:
public function pay(Paybox $paybox): JsonResponse
{
$paybox->setParameters([
'PBX_TOTAL' => 1990, // integer, in cents
'PBX_DEVISE' => '978', // ISO 4217 numeric, EUR
'PBX_CMD' => 'ORD-2024-001',
'PBX_PORTEUR' => $user->getEmail(),
'PBX_RETOUR' => 'amount:M;ref:R;auth:A;err:E',
]);
return $this->json(['url' => $paybox->getUrl(), 'params' => $paybox->getParameters()]);
}
Track B — you are on Laravel (overwhelmingly likely). Do not install. Treat the repo as executable documentation of the Paybox protocol. The realistic day-one move is a 3–5 day spike: read the two classes above, extract the signing and verification logic into your own App\Payments\Paybox namespace, and cover it with your own tests. You will end up with ~200–400 lines of straightforward PHP that you own, instead of a black-box fork you cannot upgrade.
What you actually need from the reference, and should write yourself:
PBX_* fields, canonical string ordering, hash_hmac with PBX_HASH_CHOICE (SHA512 on modern merchant contracts, SHA256 on older ones — must match the contract exactly), then bin2hex.config/services.php with site/rank/identifier/key/production pulled from env / secret manager. Mirror the Symfony config tree's shape but not its plumbing.Your first end-to-end use case is the same in both tracks: build the redirect form, verify the return on the way back, and mark the order paid. Everything else is refinement.
Redirect-then-verify is the whole model. Paybox System is a browser form-post to a hosted page, then a return post to your URL. There is no synchronous server-to-server call in scope. Design your code as two isolated handlers — one builds and signs, one verifies and reconciles — because they deploy and fail independently.
Keep controllers thin; put logic in a service. In Symfony this is what the bundle already does (config + signing in Paybox\Paybox); in Laravel replicate it with a small PayboxGateway service that returns a value object:
final class PayboxGateway
{
public function createPayment(int $amountCents, string $orderRef, string $email): PayboxRedirect
{
$params = [
'PBX_SITE' => config('services.paybox.site'),
'PBX_RANG' => config('services.paybox.rank'),
'PBX_IDENTIFIANT'=> config('services.paybox.identifier'),
'PBX_TOTAL' => $amountCents,
'PBX_DEVISE' => '978',
'PBX_CMD' => $orderRef,
'PBX_PORTEUR' => $email,
'PBX_RETOUR' => 'amount:M;ref:R;auth:A;err:E;signature:K',
'PBX_HASH' => 'SHA512',
'PBX_TIME' => now()->toIso8601String(),
'PBX_REPONDRE_A' => route('paybox.ipn'),
];
$params[' PBX_HMAC'] = $this->sign($params);
// note the leading space on PBX_HMAC — Paybox's spec is quirky here
return new PayboxRedirect(config('services.paybox.url'), $params);
}
}
Callback handling is a state machine, not a branch. Verify signature → look up order by ref → check current status → transition only if the transition is legal → persist raw payload → return 200. Anything else is a bug waiting for a retry storm.
public function ipn(Request $request): Response
{
$payload = $request->all();
if (! $this->signature->verify($payload)) {
Log::warning('paybox.ipn.bad_signature', ['payload' => $payload]);
return response('invalid', 400);
}
return DB::transaction(function () use ($payload) {
$order = Order::where('reference', $payload['ref'])->lockForUpdate()->firstOrFail();
$this->stateMachine->apply($order, $payload); // idempotent
return response('OK', 200);
});
}
Config via env, never committed. PAYBOX_HMAC_KEY lives in the secret manager. PAYBOX_PRODUCTION is a boolean toggle that flips the endpoint URL between preprod and prod — but the credentials differ per environment too, so gate the key on the same flag to avoid the classic "test key against prod endpoint" incident.
Sandbox first, always. Paybox preprod requires a real merchant contract to provision test credentials; there is no self-serve sandbox. Budget week 0 for this — it gates everything else. Without it, this package (and any port of it) is untestable in any meaningful sense.
Test the signature independently. Unit tests should cover: canonical ordering, hex encoding, hash_equals-style comparison, and rejection of a tampered field. Feature tests should cover the callback endpoint: valid callback, replayed callback, malformed payload, wrong signature, and unknown order reference. This is the only part of the integration where a subtle bug is silently expensive.
Reconciliation job. Poll Paybox (or query your order state) nightly for orders in pending_payment older than N minutes. Compare against the PSP's transaction status and mark stale orders expired. This is not optional — callbacks get lost when users close the browser or the network drops.
Extension points. Symfonically, the service is injectable and the config tree is overridable; you can decorate Paybox\Paybox to add custom PBX_* fields or logging. In your Laravel port, that maps to: gateway service bound to the container, configured via config/services.php, decorated with a PayboxLogger if you want raw payload capture. Keep the seam where you can swap signing algorithms or endpoints without touching controllers.
Namespace lies. Even after composer require gabrielextso/paybox-bundle, the namespace is Lexik\Bundle\PayboxBundle. Every use statement, every service ID, every class you grep for starts with Lexik. This trips up anyone expecting GabrielEXTSO.
It is not a Laravel package. No ServiceProvider, no facade, no publishes(). Attempting to shim Symfony DI into a Laravel container costs more than reimplementing the two classes. If you catch yourself writing a SymfonyBundleServiceProvider, stop.
Lineage rot. The upstream targeted Symfony 2.x/3.x and PHP 5.x/7.x. The fork almost certainly does not declare PHP 8.x support in composer.json. Test hash_hmac and hash_equals behaviour on your target PHP before trusting the output — these are stable, but the surrounding classes (Response, Request handling in the bundle) may not be.
hash_equals for verification, always. If the fork (or your port) uses == or strcmp for signature comparison, that is a timing-side-channel vulnerability in a payment path. Fix it, do not "note it for later." Never verify signatures in userland comparison operators.
Algorithm alignment is a config bug, not a code bug. PBX_HASH in the request must match what the merchant contract is configured for (SHA512 vs SHA256). A mismatch produces a silent signature rejection on return — no error page, just a lost payment. Log the exact canonical string and computed HMAC on mismatch (redact the key) so you can diff it against what Paybox expects.
Parameter ordering in the canonical string. The signature base string is the PBX_* pairs joined in a defined order. Getting the order wrong, or including PBX_HMAC itself, produces a valid-looking but rejected signature. Sort deterministically (Paybox's spec defines the order; do not rely on PHP array order if you build the array dynamically).
PBX_RETOUR encoding. Values in the return URL are percent-encoded and HTML-entity-escaped in places. urldecode once, not twice. Values like an amount of 19.90 become 19.90 in one hop and 19.9 after a careless floatval. Keep raw strings until you have verified and only then cast.
Amounts are integers in cents. Never floats. PBX_TOTAL is in the currency's minor unit. Currency 978 = EUR. Amount/currency mismatches are the single most common cause of "paid but order not fulfilled."
Duplicate and replayed callbacks happen by design. Paybox retries. Your handler must be idempotent — key on ref (order reference), take a row lock, compare current status, apply only if the transition is legal. Do not trust the callback's "success" flag without also checking the amount matches the order.
CSRF must be off on the callback route, but signature verification is not optional. The correct pattern is ->withoutMiddleware([VerifyCsrfToken::class]) plus HMAC verification plus, ideally, an IP allowlist for Paybox's ranges. Do not confuse "CSRF off" with "unauthenticated."
Lost callbacks need a reconciliation path. Browser closes, network drops, user refreshes at the wrong moment — the callback does not always arrive. Nightly job plus a manual admin retry (POST /admin/orders/{id}/reconcile) covers the long tail.
The callback endpoint must never 5xx during a deploy. Paybox will treat 5xx as a retry trigger and, after N retries, stop. Design the endpoint to accept, persist the raw payload, enqueue, and return 200 — even if downstream processing fails. Then process the queued job with its own retry budget.
No releases, no changelog, no tests. You cannot diff "what this fork changed" against upstream with confidence. If you fork for real: pin to a commit SHA in composer.json, vendor the source into your repo, and add the tests that were never shipped — especially on verification. Do not let this become a transitive dependency you cannot upgrade on your own schedule.
Adoption is zero. 1 star, 0 dependents, one maintainer, dead upstream. That is not a signal to trust in any package, and payment code is the worst place to make an exception. Treat the repository as a spec artifact, port the pieces you need, and own them. Budget an annual review against Verifone's current documentation and a named engineer-owner if this reaches production.
How can I help you explore Laravel packages today?