karser/robokassa-bundle
Symfony bundle for integrating the Robokassa payment gateway. It generates signed payment URLs, handles result and success callbacks, and lets you configure merchant credentials, currency, and test mode in your Symfony application.
Be clear about what you're adopting: this is a Symfony bundle, not a Laravel package. It plugs Robokassa into the (long-abandoned) JMSPaymentCoreBundle abstraction — a PaymentPluginInterface implementation plus a controller that accepts Robokassa's server-to-server callback. There is no service provider, no facade, no config publishing. If you're a Laravel dev, your realistic options are (a) run it only inside a legacy Symfony app, or (b) read Plugin/RobokassaPlugin.php and the controller, and port ~150 lines of signature-building logic into a Laravel gateway class. Option (b) is almost always the right call.
Minimal steps (Symfony context):
composer require karser/robokassa-bundle
// app/AppKernel.php
new Karser\RobokassaBundle\KarserRobokassaBundle(),
new JMSPayment\CoreBundle\JMSPaymentCoreBundle(),
# config.yml — MerchantLogin / Password1 / Password2 from your Robokassa cabinet
karser_robokassa:
login: '%robokassa_login%'
password1: '%robokassa_password1%' # used to build the payment-link signature
password2: '%robokassa_password2%' # used to verify the result callback
test: false
Import the bundle's routing so the callback endpoint exists, then point Robokassa's Result URL at it in the merchant cabinet.
Where to look first, in this order:
DependencyInjection/Configuration.php — the only authoritative list of config keys and their defaults (the README is thin here).Plugin/RobokassaPlugin.php — signature construction and the redirect/response behaviour live here.Controller/RobokassaController.php — the public callback endpoint, and what it returns to Robokassa.Resources/config/routing.xml (or .yml) — the exact route name/path you must paste into the cabinet.First use case: a single-product checkout. Build a JMSPayment PaymentInstruction for the order (amount, currency RUB, description), call the plugin's approve()/deposit() to get a RedirectResponse to https://auth.robokassa.ru/Merchant/Index.aspx?..., send the user there, and let the result callback flip your order to paid.
Signature generation is the whole integration. Everything else is plumbing. Build the payment-link signature over MerchantLogin:OutSum:InvId:Password1, MD5, and append SignatureValue= to the redirect URL. The result callback signature is OutSum:InvId:Password2 — note the different password and the absence of the login. If you add custom parameters (Shp_*), they are appended to the signed string in alphabetical key order, keys included, for both signatures.
$sig = md5(sprintf('%s:%s:%s:%s', $login, $outSum, $invId, $password1));
$url = "https://auth.robokassa.ru/Merchant/Index.aspx?MerchantLogin={$login}"
. "&OutSum={$outSum}&InvId={$invId}&Description=" . urlencode($desc)
. "&SignatureValue={$sig}" . ($test ? '&IsTest=1' : '');
InvId is your correlation key, and it's an integer. Robokassa echoes it back verbatim, so make it your order's primary key (or a dedicated payment_attempts.id). Do not use a UUID or a string with a prefix — the signature is computed over the literal value and any merchant cabinet filters will reject non-numeric IDs.
Callback handler pattern (framework-agnostic, works in Laravel verbatim):
public function result(Request $r)
{
$outSum = $r->input('OutSum');
$invId = $r->input('InvId');
$sig = strtoupper($r->input('SignatureValue'));
$expected = strtoupper(md5("{$outSum}:{$invId}:{$this->password2}"));
if (!hash_equals($expected, $sig)) {
abort(403); // log loudly; this is either an attack or a password mix-up
}
$order = Order::lockForUpdate()->findOrFail($invId);
if ($order->status !== 'paid') { // idempotency: Robokassa WILL retry
$order->markPaid((float) $outSum);
}
return response("OK{$invId}", 200)->header('Content-Type', 'text/plain');
}
Two return legs, different jobs. SuccessUrl and FailUrl are browser redirects — the user lands there, so render a thank-you page and never mark the order paid from them (they're trivially spoofable). The ResultUrl is the only trusted signal. In Symfony terms: successAction/failAction vs. resultAction; in Laravel: three distinct routes, with only the result route excluded from CSRF and auth middleware.
Route hygiene for the callback. Robokassa posts application/x-www-form-urlencoded with no session and no referrer. The endpoint must be:
VerifyCsrfToken (Laravel) / reachable anonymously (Symfony IS_AUTHENTICATED_ANONYMOUSLY),Wiring into your domain. Keep the gateway thin: it should translate Robokassa's payload into a domain event (OrderPaid) and return the OK{InvId} string. Anything else (email, provisioning, stock decrement) belongs in a queued listener so the callback returns in milliseconds.
Test mode is a config flag with a trap. Robokassa's test mode uses the test password pair and adds IsTest=1. Set the flag from an env var (ROBOKASSA_TEST), not a hardcoded literal, and make sure the test/production passwords are swapped together with the flag — a half-swapped config produces SignatureValue mismatches that look like code bugs.
Symfony 2/3 vintage, and it shows. Last release April 2018, 16 stars, zero dependents. It predates Symfony 4's bundle-less structure, so expect bundle registration in AppKernel, Resources/config YAML/XML rather than PHP config, and no @Route attributes. Getting it onto Symfony 5+ means rewriting the DI extension and controller; there is no maintained fork to lean on. For a Laravel app, treat the repo purely as reference material and budget an afternoon to port — you'll write less code than the dependency would add.
JMSPaymentCoreBundle is the bigger risk than this bundle. It's been unmaintained far longer. Adopting karser/robokassa-bundle means inheriting that whole abstraction (PaymentInstruction, PluginController, ExtendedData) for a single gateway. If you only need Robokassa, the abstraction buys you nothing and costs you a fragile transitive dependency.
Signature case and whitespace are the #1 support ticket. MD5 hex output is lowercase; Robokassa compares case-insensitively but many implementations compare strictly. Normalize with strtoupper() on both sides. Equally common: OutSum arriving as 1500.00 while you signed 1500, or a localized comma decimal separator sneaking in via number_format(). Format amounts explicitly with . and two decimals before signing, and sign the exact string you send.
Shp_ parameters must match exactly on both legs. If you sign with Shp_order=42&Shp_user=7, Robokassa echoes them back and includes them in the callback signature. Order matters (alphabetical by the full key including the Shp_ prefix), and a mismatch between the parameters you send and the ones you verify against is silent — you just get a failed signature with no hint why.
Return body is literal. OK{InvId} — no JSON envelope, no trailing newline from a stray ?> or a Blade template, no BOM. If your framework's response helper appends whitespace or a debug toolbar injects HTML, Robokassa considers the notification unacknowledged and retries. Return text/plain and nothing else.
Robokassa retries aggressively and out of order. Notifications can arrive multiple times, and a success page visit can precede the result callback. Make the handler idempotent (guard on order state, lockForUpdate), and never assume success means "already processed."
Fiscalization (Receipt) is not handled by this bundle. Russian merchants selling to individuals must pass a Receipt JSON parameter (items, VAT, taxation system) in the payment link since 2019. The 2018 bundle predates enforcement; you'll be building that payload and its signature yourself. Budget for it — it's the most common reason these integrations get rewritten.
Debugging playbook: log the raw POST body and the exact string you hashed (not just the hash) on every failed comparison. Nine times out of ten the diff is a password from the wrong environment, an amount format change, or a Shp_ ordering issue. Also confirm you're hitting auth.robokassa.ru (production) vs. the test endpoint consistently with your password set.
Extension points, if you must stay on the bundle: decorate the plugin service (decoration: karser_robokassa.plugin) to add receipt parameters or logging without forking; override the callback route in your app's routing so a local controller supersedes the bundle's; and hang a kernel.event_listener on the JMSPayment events rather than editing vendor code. All of these are workarounds around an abandoned dependency — weigh them against a 150-line in-house implementation, which is what most teams end up with anyway.
How can I help you explore Laravel packages today?