whatwedo/esr-bundle
Symfony bundle for generating Swiss ESR payment slips (Einzahlungsscheine), with reference-number handling and printable output to support automated payment reconciliation in Symfony applications.
whatwedo/esr-bundle is not a Laravel package — it is a Symfony 2-era bundle that wraps Swiss ESR (orange Einzahlungsschein) PDF generation, later extended with Swiss QR-invoice support. Treat it as a reference implementation you mine rather than something you composer require into a Laravel app.
Look here first, in this order:
composer.json — tells you which PDF engine (TCPDF/FPDF/mPDF) and which QR library it delegates to. That dependency, not the bundle, is what you actually want to reuse.DependencyInjection/Configuration.php — the canonical list of config keys (account/IBAN, client number, reference prefix, company address, fonts, PDF options). This is your source of truth for a Laravel config/esr.php.Resources/config/services.* — the service IDs and constructor arguments, i.e. the wiring you must reproduce by hand in a service provider.src/ — the generator class itself. If it has no ContainerAware/ContainerInterface coupling, instantiate it directly and skip the bundle entirely.Install attempt (expect friction):
composer require whatwedo/esr-bundle --ignore-platform-reqs
composer why-not whatwedo/esr-bundle # see which Symfony/PHP constraint blocks you
If the constraints are Symfony 2.x / PHP 5.x–7.x, do not fight it. Copy the generator source into app/Support/Esr/ (MIT license permits this, keep the header) and wire it yourself.
First use case — a single slip behind a download route:
// app/Services/Esr/InvoiceSlipGenerator.php
public function pdf(Invoice $invoice): string
{
$data = [
'account' => config('esr.iban'),
'client' => config('esr.client_number'),
'reference' => $this->reference($invoice), // ESR reference w/ check digit
'amount' => number_format($invoice->total, 2, '', ''), // 8 digits, no separator
'currency' => 'CHF',
'debtor' => $invoice->customer->toEsrAddress(),
];
return $this->renderer->render($data); // returns raw PDF bytes
}
// routes/web.php
Route::get('/invoices/{invoice}/slip', fn (Invoice $invoice) =>
response($generator->pdf($invoice), 200, ['Content-Type' => 'application/pdf'])
);
Validate the output once against the official Swiss QR validation portal (or PostFinance's checker) before you build anything on top of it.
1. Config port, not config import. Mirror the bundle's configuration tree into config/esr.php and feed it from .env. Keep the bundle's array shape so upstream fixes remain easy to port.
// config/esr.php
return [
'iban' => env('ESR_IBAN'),
'qr_iban' => env('ESR_QR_IBAN'), // required for QR invoices
'client_number' => env('ESR_CLIENT_NUMBER'),
'company' => ['name' => ..., 'street' => ..., 'zip' => ..., 'city' => ...],
'reference_prefix' => 'RF',
'pdf' => ['font' => 'ocrb', 'margin_top' => 0],
];
2. Wrap the renderer behind an interface. One method in, PDF bytes out. That single seam lets you swap the legacy ESR slip for the newer QR-bill library later without touching controllers.
interface PaymentSlipRenderer {
public function render(PaymentSlipData $data): string;
}
3. Map Eloquent → value object, never pass models in. Build a small DTO (PaymentSlipData) from your Invoice/Customer models. Keeps the copied generator free of Laravel coupling and makes it testable.
4. Reference number generation is the core business logic. The ESR reference is client_number + invoice_number, padded, then a recursive mod-10 check digit appended:
private const TABLE = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5];
public function checkDigit(string $digits): int
{
$carry = 0;
foreach (str_split($digits) as $d) {
$carry = self::TABLE[($carry + (int) $d) % 10];
}
return (10 - $carry) % 10;
}
For QR invoices, choose reference type deliberately: QRR (27-digit ESR reference, needs QR-IBAN) vs SCOR (ISO 11649 RF..., works with a normal IBAN). Persist the reference on the invoice row so it never drifts after edits.
5. Generate asynchronously for anything bulk. PDF rendering is CPU-bound and the legacy libraries are slow. Throw a job, write to Storage, and let the download route serve the file:
dispatch(new GeneratePaymentSlip($invoice))->onQueue('pdf');
6. Integration tip — isolate if you must run the bundle. If you truly need the bundle's assembled services, run a minimal Symfony kernel in a separate container and call it over HTTP from Laravel. Do not boot a Symfony kernel inside a Laravel request; the container, event dispatcher, and Twig setups will collide.
7. Tests that actually catch regressions:
$pdf = $renderer->render($fixture);
$this->assertStringStartsWith('%PDF', $pdf);
$this->assertSame('210000000003139471430009017', $data->reference); // golden value
Golden-value tests on the reference number and a byte-sniff on the PDF are the highest-value assertions. Optionally render page-to-image and diff against a known-good slip.
8. Overriding output. The bundle renders through Twig templates in Resources/views. When you extract the code, copy those templates into resources/views/vendor/esr/ and keep the same variable names — that is the easiest extension point for logos and address layout.
symfony/*: ~2.8 pins. --ignore-platform-reqs may install but will fatal at runtime on PHP 8.1+ (dynamic properties, each(), create_function, TCPDF/FPDF deprecations). Plan on copying the source.00001250 = CHF 12.50). Currency is CHF only. Passing 12.5 or "12.50" produces a slip the bank rejects.[0,9,4,6,8,2,7,1,3,5]; a hand-rolled "mod 10" from another country's spec will generate valid-looking but invalid references. Test with known-good numbers from the PostFinance spec.Configuration.php defaults are not documented anywhere else. Any key you omit silently takes the bundle default, which is how blank company names and wrong account numbers slip into production. Port all keys, even the ones you think you don't need.PaymentSlipRenderer interface.vendor/ in place.How can I help you explore Laravel packages today?