rikudou/czqrpayment
PHP package for generating Czech QR payment codes (QR Platba) from account, amount, and payment details. Produces standards-compliant QR strings for banking apps, with support for IBAN, variable/specific/constant symbols, and Laravel-friendly integration.
Install
composer require rikudou/czqrpayment:^5.3
# only if you want raster images (PNG) instead of SVG:
composer require endroid/qr-code
There is no Laravel service provider, facade, or config file — it's a plain, framework-agnostic library. You wire it up yourself in AppServiceProvider or, better, in a small app-level service class.
Where to look first
README.md — usage block for CzechQrPayment (the only class you'll touch 95% of the time).QrPayment interface — this is your contract; code against it, not the concrete class, so you can swap in SlovakQrPayment/EuropeanQrPayment or your own implementation later.getQrString() (the raw SPD payload, e.g. SPD*1.0*ACC:CZ65...*AM:1234.50*CC:CZK*X-VS:2024001234*MSG:...) and getQrCode() (a QR image object you can render as PNG/SVG).setOptions() — v5.3.x added/covered the instant payment option here (see Implementation Patterns #8). Czech instant payments are a real user-facing difference, so check the README for the exact option key/setter before you build on it.Smallest possible first use case — a QR code on an invoice PDF:
use Rikudou\CzQrPayment\CzechQrPayment;
$payment = new CzechQrPayment();
$payment
->setAccount('CZ6508000000192000145399')
->setAmount(1234.50)
->setCurrency('CZK')
->setVariableSymbol('2024001234')
->setRecipientName('Acme s.r.o.')
->setMessage('Faktura 2024001234');
$spd = $payment->getQrString(); // log this — it's the whole point
$qr = $payment->getQrCode(); // render/attach
Method names above follow the README as of the v5.3.1 release; confirm the exact setter names (
setVariableSymbolvssetVariableSymbolString, etc.) and thesetOptions()keys against the README before you build on them.
Assessment up front: the library's real output is a string; the QR encoding is delegated to bacon/bacon-qr-code / endroid/qr-code. So its value is SPD formatting plus validation of the Czech payment rules. 29 stars and 0 dependents means bus-factor risk, but the underlying format (Czech Banking Association "QR Platba" / SPD 1.0) is a frozen public standard — worst case you reimplement the ~60 lines of payload building and keep everything else. Treat it as vendorable, not load-bearing.
Release cadence note (v5.3.1): this was a patch release — test coverage for instant payment, a funding link, a docs link to a Hungarian QR generator, and a bump of the required QR code provider version. No new public API names, no deprecations. The only actionable item is the dependency floor: run composer update rikudou/czqrpayment --with-dependencies, then diff composer.lock and re-run your getQrCode() render test (see Gotchas).
1. Wrap it — never call it from a controller. One service that takes a domain object and returns a DTO with { spd, payload }:
final class QrPaymentBuilder
{
public function forInvoice(Invoice $invoice): QrPayload
{
$p = new CzechQrPayment();
$p->setAccount(config('payments.iban'))
->setAmount($invoice->total_halere / 100)
->setCurrency('CZK')
->setVariableSymbol($invoice->variable_symbol)
->setRecipientName(config('payments.recipient_name'))
->setDueDate($invoice->due_at) // YYYYMMDD in the payload
->setMessage("Faktura {$invoice->number}");
return new QrPayload($p->getQrString(), $p->getQrCode());
}
}
This is your swap point if you ever move to EPC QR (EUR) or a Slovak PAY by square format — the rest of the app never sees the library.
2. Bind per-resolution, not as a singleton. The object is mutable state; a shared instance will leak the previous invoice's amount into the next request under Octane/queues.
$this->app->bind(QrPaymentBuilder::class);
// or ->scoped() if you're on Octane and need per-request caching
3. Blade component emitting inline SVG. Best for PDFs (dompdf/snappy), retina screens, and emails — no GD dependency, no temp files:
{{-- resources/views/components/qr-payment.blade.php --}}
<svg {!! $qr->getSvgAttributes() !!}>
{!! $qr->getSvgBody() !!}
</svg>
The exact SVG accessors come from the QR provider, not from this package — v5.3.1 bumped that provider's required version. If the properties/methods differ on your resolved version, fall back to
$qr->writeString()into adata:URI or write the SVG to storage once per invoice.
For a downloadable/embeddable endpoint, a signed route is the cleanest:
Route::get('/pay/{invoice:uuid}/qr.svg', PaymentQrController::class)
->middleware('signed')
->name('pay.qr');
Cache the generated payload on the model and bust it with a hash of the mutable inputs (amount, VS, currency, due date, options), so a corrected invoice doesn't ship a stale QR.
4. Persist the SPD string on the invoice. Store spd_string + spd_hash. It's ~150 bytes, it makes the QR reproducible from the DB alone (no re-deriving business rules), and it's the single best debugging artifact you'll have. It also makes provider upgrades a non-event: existing invoices keep rendering, only new ones use the new builder.
5. Money handling. Keep amounts as integer haléře; convert with number_format($halere / 100, 2, '.', ''). Never feed a localized string — 1 234,50 will be misinterpreted or rejected.
6. Generate once, reuse in mail + PDF. Build the payload when the invoice is issued (a queued job or model observer), attach the SVG/PNG to the mailable, and let the PDF view read the stored string.
7. Test the string, not the pixels. QR PNGs are effectively unassertable; the SPD payload is not:
$this->assertStringContainsString('*AM:1234.50*', $payload->spd);
$this->assertStringContainsString('*X-VS:2024001234*', $payload->spd);
Decode a few real payloads manually in a bank app once during development, then never again.
8. Consider the instant-payment option explicitly. The 5.3.x line exposes an instant payment flag through setOptions(). Behaviour differs by bank — the QR still scans everywhere, but the payer's app may preselect an instant/express scheme. Make it a config flag rather than hardcoding it, so finance can toggle it without a deploy:
$p->setOptions([
// check README for the exact instant-payment key on v5.3.x
]);
Include it in the hash you cache the payload against — flipping the flag must invalidate stored QRs, or you'll be shipping the old preference forever.
9. Upgrade smoke test. After any 5.x bump, generate one known-good payload in a feature test and assert the full string with assertSame(), then render the image once. That catches provider-bump regressions (SVG/PNG writing, encoding mode) without pinning you to pixel diffs.
CzechQrPayment instance carries AM, X-VS, MSG, and options across requests.composer require gives you nothing new in config/ or php artisan about. Don't hunt for a config file; publish your own config/payments.php with IBAN/recipient.endroid/qr-code (or an old bacon/bacon-qr-code) elsewhere, Composer may resolve an older pair or conflict outright. Upgrade both together (--with-dependencies) and re-run the render test; don't let a transitive QR-provider downgrade silently change your SVG output.Str::ascii() yourself so behaviour is identical across upgrades.MSG ~60 chars, RN ~35, VS/SS/KS ≤10 digits. Truncate in the builder and log when you do; a silently truncated message on 10,000 invoices is a support nightmare.* is the field separator — never let it into a message or recipient name. Validate with a regex on your builder inputs.123456789/0100 must be converted to CZ…. Use rias/cz-iban or implement the mod-97 conversion once in a helper — don't do string surgery per call site.BCD\n002\n1\nSCT…), a completely different payload that Czech/Slovak banking apps often won't scan the same way. If you need EUR, use a separate implementation and a separate QR code — do not stuff CC:EUR into SPD and hope. (The v5.3.1 docs link to a Hungarian generator; that's a different national format again — useful for reference, not for reuse.)getQrCode()->writeString() then dies at runtime. Prefer SVG, or add the extension to your Dockerfile and smoke-test the route.getQrString(), paste it into any SPD/QR decoder, and compare field-by-field. Faster than reading the library source.QrPaymentRenderer interface rather than post-processing bytes. Your own QrPayload DTO is the better long-term seam, though."rikudou/czqrpayment": "^5.3" — a single-maintainer package at ~29 stars can ship a breaking minor; don't float on *. Treat patch bumps as safe but still run your one-payload assertSame test, since patch releases can (and here did) move transitive dependencies.How can I help you explore Laravel packages today?