Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Euqrpayment Laravel Package

rikudou/euqrpayment

Generate EU QR payment codes (EPC/SEPA) for invoices and payment slips. This Laravel/PHP package builds compliant QR payloads with IBAN, BIC, amount and remittance data, plus validation helpers and simple Blade rendering.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require rikudou/euqrpayment

That's the whole install. There is no service provider, no facade, no config to publish, no migrations. It is a plain PHP string builder: you construct a QrPayment object, set the EPC fields, and call getQrString() to get the newline-delimited payload that goes into a QR code.

Where to look first

  1. The README on GitHub — it contains the full example and the list of setters. Read it before anything else; there is no docs site and the class is small enough to read end-to-end in five minutes.
  2. src/QrPayment.php — read the setter names and any validation exceptions it throws. That is effectively the entire API surface.
  3. The EPC standard itself (EPC069-12, version 2) — the package implements the wire format, but you are responsible for feeding it valid data.

First use case

The canonical use case is "customer pays a SEPA invoice by scanning a QR code with their banking app" (Czech, German, Austrian, Belgian, Finnish banking apps broadly support it; coverage is uneven elsewhere in the EU).

use Rikudou\EuQrPayment\QrPayment;

$payment = new QrPayment('CZ'); // country code, used for IBAN/BIC expectations

$payload = $payment
    ->setName('Acme s.r.o.')
    ->setIban('CZ6508000000192000145399')
    ->setBic('GIBACZPX')
    ->setAmount(1250.00)
    ->setCurrency('EUR')
    ->setRemittance('Invoice 2024-0042')
    ->setInformation('Thank you for your order')
    ->getQrString();

At this point you have the raw EPC string, e.g.:

BCD
002
1
SCT
GIBACZPX
Acme s.r.o.
CZ6508000000192000145399
EUR1250.00


Invoice 2024-0042
Thank you for your order

To turn it into an actual image, you need a second library (endroid/qr-code, bacon/bacon-qr-code, chillerlan/php-qrcode) — this package deliberately does not render pixels.

composer require endroid/qr-code
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Writer\PngWriter;

$result = Builder::create()
    ->writer(new PngWriter())
    ->data($payload)
    ->errorCorrectionLevel(ErrorCorrectionLevel::Medium)
    ->size(400)
    ->margin(16)
    ->build();

file_put_contents(storage_path('app/invoices/qr-42.png'), $result->getString());

Render it in Blade as data:image/png;base64,... or drop it into a PDF invoice (dompdf/snappy).


Implementation Patterns

Wrap it in a dedicated builder / action

Because the library is a bare value object, put the mapping from your domain (an Invoice/Order model) into it behind one class. This is the single most valuable thing you can do, since it isolates you from both the library's age and its API.

namespace App\Services;

use App\Models\Invoice;
use Rikudou\EuQrPayment\QrPayment;

class InvoiceQrPayload
{
    public function __construct(private Invoice $invoice) {}

    public function string(): string
    {
        $p = new QrPayment(config('payments.qr.country'));

        $p->setName(config('payments.qr.beneficiary_name'))
          ->setIban(config('payments.qr.iban'))
          ->setBic(config('payments.qr.bic'))
          ->setAmount((float) $this->invoice->total)
          ->setCurrency('EUR')
          ->setRemittance('Invoice ' . $this->invoice->number)
          ->setInformation(mb_substr($this->invoice->reference, 0, 70));

        return $p->getQrString();
    }
}

Then a Blade component / @php helper and a controller route GET /invoices/{invoice}/qr.png that returns the rendered PNG with a long cache header.

Config-driven defaults

Ship your own config/payments.php (the package provides none):

return [
    'qr' => [
        'iban'             => env('QR_IBAN'),
        'bic'              => env('QR_BIC'),
        'beneficiary_name' => env('QR_BENEFICIARY_NAME', config('app.name')),
        'country'          => env('QR_COUNTRY', 'CZ'),
    ],
];

The library's constructor takes a country code — it's used for validating/formatting expectations around IBAN and BIC. Keep it in config, not hardcoded, so a second legal entity in another country doesn't require a code change.

Form request / DTO boundary

If you ever let users paste an IBAN/BIC (e.g. a marketplace where the seller defines payout details), validate before constructing the object, because the library's validation is minimal and its exception messages are terse:

$request->validate([
    'iban' => ['required', 'string', 'regex:/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/'],
    'bic'  => ['required', 'string', 'regex:/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/'],
    'amount' => ['required', 'numeric', 'min:0.01', 'max:999999999.99'],
]);

Use brick/money or moneyphp/money for the amount and cast to string with number_format($minor / 100, 2, '.', '') — never pass a float formatted by (string) because locale settings can leak into it.

Caching strategy

Generating the string is essentially free (string concatenation), so don't over-engineer. The PNG rendering is the only mildly expensive part. Cache the PNG on disk keyed by invoice number + a content hash, or memoize at the HTTP layer:

return response(file_get_contents($path), 200, [
    'Content-Type'  => 'image/png',
    'Cache-Control' => 'public, max-age=31536000, immutable',
]);

Testing

Assert against the exact byte string. This catches field-order and newline regressions, which are the only things that realistically break in production:

public function test_invoice_payload_matches_epc_v2(): void
{
    $invoice = Invoice::factory()->create(['number' => '2024-42', 'total' => 10.50]);

    $this->assertSame(
        "BCD\n002\n1\nSCT\nGIBACZPX\nAcme s.r.o.\nCZ6508000000192000145399\nEUR10.50\n\n\nInvoice 2024-42\n",
        (new InvoiceQrPayload($invoice))->string()
    );
}

Also add one "smoke" test that scans a rendered PNG with a QR decoder in CI if you can — khanamiryan/qrcode-detector-decoder works. It will not tell you the QR is valid EPC, but it will catch a corrupted render pipeline.


Gotchas and Tips

The package is old and unmaintained — treat it as vendored code

Last release was September 2021, 18 stars, 0 dependents. It is unlikely to receive fixes for a newer EPC spec revision or a PHP 8.x edge case. Pin it exactly in composer.json, and keep the wrapper class from above so a future swap (or a fork) is a one-file change. Don't spread new QrPayment(...) across ten controllers.

EPC v2 is EUR-only

SEPA credit transfers via EPC QR are euro-only. If you setCurrency('CZK') you'll get a syntactically valid string that every banking app will reject on scan. Enforce EUR at your config/DTO layer and log if it's ever not.

Amount formatting is on you

The standard requires a plain decimal with a . separator and no thousands separator, or an empty field if no amount. Passing 1290.5 or "1 290,50" produces a bad code. Format explicitly:

->setAmount((float) number_format($cents / 100, 2, '.', ''))

Also: many banking apps treat amount as optional and let the payer edit it — if you need a fixed amount, test with the actual apps your customers use, not just one.

Remittance: structured XOR unstructured

The spec's two remittance fields are mutually exclusive — you set either an unstructured text (≤140 chars) or an ISO 11649 structured reference (RF..., ≤35 chars). Setting both usually means the library writes only one, and the field you thought you set silently disappears. Decide which one your accounting needs (structured references reconcile better; free-text is friendlier to consumers) and only ever call one setter.

Newlines and exotic characters will corrupt the payload

The payload is newline-delimited. Any \n or \r inside a name, remittance, or information field shifts every subsequent field and makes the code unreadable. Sanitize:

$text = preg_replace('/[\r\n]+/', ' ', $text);
$text = mb_substr(trim($text), 0, 70); // 70 for name/information, 140 for remittance

The spec also constrains the character set. Czech/German/Polish diacritics mostly survive UTF-8, but emoji, em-dashes, smart quotes, and non-Latin scripts will confuse scanners. A conservative Str::ascii() on name fields is often safer than trusting every bank app.

Field length limits are part of the spec, not the library

Name ≤ 70, information ≤ 70, unstructured remittance ≤ 140, structured reference ≤ 35. The package may not enforce all of these, and a too-long field can silently truncate or produce garbage. Truncate at your boundary with mb_substr, not substr (byte truncation can split a multibyte character and make the whole QR invalid UTF-8).

BIC handling

In EPC v2 the BIC is mandatory and lives in its own field, before the beneficiary name. Some older v1 codes omitted it. If you generate for a bank that only reads v1, you may need to test against that specific app — but do not omit the BIC on the assumption that "IBAN is enough"; the field order will break.

Rendering: the QR must be big and quiet

The EPC guidance recommends error-correction level M and a printed size of at least ~3 cm with a quiet zone (the white margin around the code, ~4 modules). A 100 px PNG in a corner of an A4 PDF will not scan from a phone. Use ≥ 300×300 px at render time and keep margin(16) or higher in endroid.

Debugging checklist when a scan fails

  1. dd($payload) and inspect each line — count the newlines and confirm the field order (BCD, 002, 1, SCT, BIC, name, IBAN, amount, purpose, remittance, information, language).
  2. Check for a stray blank line where amount/purpose should be — an empty field must still be a newline.
  3. Open the rendered PNG and confirm it's not a placeholder / stale cached file.
  4. Scan with two different banking apps. If one works and one doesn't, the payload is probably valid but a field (usually amount or charset) is on the edge of tolerance.
  5. Verify your source data has no trailing spaces — some scanners tolerate them, some don't.

Extension points

There aren't any — no events, no interfaces, no formatters to override. The effective extension point is your wrapper class. If you need behavior the library doesn't offer (e.g. validating IBAN checksum, supporting a second spec version, or emitting the language field), implement it in the wrapper by post-processing the string or by writing your own payload builder entirely. The format is ~12 lines; a hand-rolled builder is a legitimate alternative if you find yourself fighting this one.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource