rikudou/qr-payment-interface
A PHP interface package defining a common contract for generating QR payment codes. Implement it in your library to support standardized QR payments such as Czech QR Platba, letting applications swap implementations without changing their code.
This is a plain PHP library, not a Laravel package — there is no service provider, no config file, and no facade. Treat it as a small utility you wire up yourself.
1. Install the interface plus an implementation. The interface package on its own is inert; it only declares the contract. You need at least one concrete implementation:
composer require rikudou/qr-payment-interface
composer require rikudou/qr-payment-cz # Czech QR Platba (SPAYD)
# and/or
composer require rikudou/qr-payment-sk # Slovak Pay by Square
Pin exact versions ("rikudou/qr-payment-interface": "1.*"). With 3 stars, 0 dependents and a last release in April 2021, you are the maintainer of last resort — a surprise minor bump should never reach production unreviewed.
2. Read src/QrPaymentInterface.php first. It is a short file and it is the entire public surface: fluent setters for IBAN/BIC, amount, currency, variable/specific/constant symbol, due date, recipient name and comment, plus the two output methods (getQrString() for the raw payload, getQrImage()/getQrCode() for a QR). Everything else in the ecosystem is an implementation detail of that contract. Read the concrete class second — that's where the format-specific quirks live.
3. First use case: an invoice payment QR. The canonical flow is "take an Eloquent invoice, produce a QR string, render it on the PDF or Blade view":
use Rikudou\QrPayment\QrPaymentInterface;
$payment = app(QrPaymentInterface::class);
$payment
->setIBAN($invoice->iban)
->setAmount($invoice->total)
->setCurrency('CZK')
->setVariableSymbol((string) $invoice->number)
->setDueDate($invoice->due_at)
->setComment("Faktura {$invoice->number}");
return $payment->getQrString(); // SPD*1.0*ACC:...*AM:...*CC:CZK*X-VS:...
The generated string is a plain ASCII payload, which makes it trivially cacheable and testable — assert on substrings in your test suite rather than on a rendered image.
Bind the interface explicitly. Since there is no auto-discovery, put the binding in a dedicated PaymentServiceProvider. Prefer bind over singleton — the setters are mutable and fluent, so a singleton will leak state between requests (and badly under Octane):
$this->app->bind(QrPaymentInterface::class, function () {
return new CzechQrPayment();
});
Resolve by country, not by hardcoding. Most apps that need this need both CZ and SK, and the two formats are not interchangeable. Model that with a small factory keyed off the invoice's country/currency, and keep the interface as the return type so callers never branch:
final class QrPaymentFactory
{
public function for(string $country): QrPaymentInterface
{
return match (strtoupper($country)) {
'CZ' => new CzechQrPayment(),
'SK' => new SlovakQrPayment(),
default => throw new UnsupportedCountry($country),
};
}
}
Assemble, don't inline. The six or seven setter calls above are the kind of thing that gets copy-pasted into three controllers and then drifts. Extract an InvoiceQrPaymentAssembler (or a toQrPayment() method on a dedicated DTO) that owns the mapping from your domain model to the interface. That single class becomes the one place you touch if the package is ever abandoned and replaced.
Render, don't store. Generate the string on demand and cache it keyed by (invoice_id, updated_at). For Blade, either base64-encode the image or — better — use the string with your existing frontend QR library so you avoid the GD/Imagick dependency entirely in web requests.
Test against the interface, not the package. Because everything is behind QrPaymentInterface, you can write a FakeQrPayment in tests/Support that records setter calls and returns a fixed string. Use it in feature tests for invoices and keep a small number of integration tests that hit the real implementation and assert on the actual SPAYD payload shape.
Validate before you generate. The library will happily serialize an IBAN with spaces in it or a currency in lowercase. Add a FormRequest/rule layer (iban, ISO-4217 uppercase currency, numeric-only symbol fields) so failures surface as validation errors rather than as a QR code that a banking app silently rejects.
No Laravel integration means no safety net. No facades, no config publishing, no exception handling, no logging by default. Wrap the construction in your own factory as above so that the rest of the application has exactly one import of a Rikudou\ namespace.
getQrImage() couples you to endroid/qr-code. The concrete implementations return an object from that package, and its major versions have broken BC repeatedly. If your app (or another dependency) needs a different major, Composer will deadlock or you'll get a runtime type error. Mitigation: only ever call getQrString() and generate the image with whatever QR library you already have. The string is the actual value of this package; the image helper is incidental.
Image generation needs ext-gd or ext-imagick plus ext-mbstring. The Slovak Pay by Square format additionally involves binary serialization and LZMA-style compression, so its requirements are heavier than the Czech one. If you only need CZ, do not install the SK package — you're just buying yourself a dependency-conflict surface.
Statefulness is the top production bug. Fluent mutable setters mean a resolved instance carries the previous invoice's data. Never store a QrPaymentInterface on a singleton, a queue worker property, or a Livewire component. Construct fresh per use.
Amount and currency formatting are unforgiving. SPAYD expects a decimal point, at most two decimals, and no thousands separator; currency must be an uppercase ISO-4217 code. Passing a float 100.0 where the implementation writes 100 vs 100.00 matters to some bank parsers. Feed it a string you control (number_format($x, 2, '.', '')) rather than trusting the cast.
Symbols are numeric strings with length limits and meaningful leading zeros. (string) the column, don't pass an int, and check the max lengths (variable symbol in particular). A cast that strips a leading zero produces a QR code that pays the wrong reference.
Due dates are date-only in the payload. Timezone drift around midnight is a real failure mode — pass a date in the invoice's own timezone, not now() in UTC.
Debugging is easy, which is the package's redeeming feature. The output is human-readable (SPD*1.0*ACC:...*AM:...*CC:...*X-VS:...), so ddd($qrPayment->getQrString()) in a Tinker session tells you immediately which field is wrong. Compare against the Czech Banking Association's SPAYD spec or the Slovak Pay by Square spec rather than against the package's own tests — the tests encode 2021 assumptions.
Treat 2021 as the ceiling. No releases since April 2021, three stars, zero dependents: there is no security posture, no PHP 8.4 guarantee, and no upstream to escalate to. Keep the blast radius to one assembler class, pin versions, and consider vendoring the concrete class into your own namespace if the interface is genuinely load-bearing for payments — a QR typo is a support ticket, not an outage, but it is not a fun one.
How can I help you explore Laravel packages today?