jonasarts/phpqrcode-bundle
Symfony bundle that integrates the PHP QR Code library, making it easy to generate QR codes from your application and templates. It provides a convenient wrapper around QR generation, so you can add scannable codes for URLs, contact details, or other content with minimal setup and configuration.
Calibrate the fit before you write a line of integration code: this is a Symfony bundle, not a Laravel package. There is no service provider, no auto-discovery, no facade, no vendor:publish. If you composer require jonasarts/phpqrcode-bundle in a Laravel app, the install will succeed (Laravel depends on Symfony components, so the constraints resolve) but nothing will register — no service, no config node, no Twig function. You'll spend an afternoon discovering it's inert.
So "getting started" on Laravel is really one of three concrete moves:
DependencyInjection/*Extension.php (the config node — canonical list of keys and types) and the service definition under Resources/config/ (the service ID and the exact method surface). That's the whole package. Skim the Twig extension last — it shows you what a rendered, embedded code is supposed to look like.app/Services/QrCodeGenerator.php, register it in a service provider, and require the upstream phpqrcode encoder as the backing library. This is roughly 50 lines, and it's the honest cost of using anything from this repo on Laravel.simplesoftwareio/simple-qrcode (Laravel-native, wraps bacon/bacon-qr-code, QrCode::size(300)->generate($text)) or chillerlan/php-qrcode (framework-agnostic). Same integration effort, maintained dependency. This is the default recommendation.If you are on Symfony, the path is straightforward:
composer require jonasarts/phpqrcode-bundle
Register the bundle manually in config/bundles.php unless a Flex recipe exists (check the repo — a 6-star bundle almost certainly doesn't ship one), then configure in config/packages/jonasarts_phpqrcode.yaml. Read the extension class to confirm the exact key names and value types (is error_correction an int 0–3 or a string L|M|Q|H?); README examples and reality drift.
First use case — the one you'll hit 90% of the time: encode a URL into a PNG for an invoice, event ticket, or 2FA handoff.
final class InvoiceController
{
public function __construct(private QrCodeGenerator $qr) {}
public function qr(int $id): Response
{
$png = $this->qr->png("https://app.test/invoices/{$id}");
return new Response($png, 200, ['Content-Type' => 'image/png']);
}
}
Where to look first, in order: (1) the DI extension class — the definitive config surface; (2) the service class — method names and return types (raw bytes vs. data URI vs. file path); (3) the Twig extension — shortest path to seeing how a rendered code is meant to be embedded.
Prerequisite check: php -m | grep gd. The upstream encoder is GD-only; absence is a fatal, not an exception, and you want to know that before you write a controller.
1. One method per output mode, not one method plus a config flag. The package offers PNG, data URI, and file. Wrap each as a distinct method on your adapter — a format string threaded through config is easy to misuse and hard to test:
interface QrCodeGenerator
{
public function png(string $payload): string; // raw bytes
public function dataUri(string $payload): string; // data:image/png;base64,...
public function write(string $payload, string $path): void;
}
2. Inline rendering belongs in a controller or a view model, not a Blade template. Symfony ships a Twig helper and it's fine for one-liners (<img src="{{ qrcode_data_uri(url(...)) }}">). Laravel has no equivalent — expose the data URI from a controller (return response()->json(['qr' => $this->qr->dataUri($url)])) or pass it in as a view variable. Never call the encoder inline in Blade: it couples rendering to encoding, defeats response caching, and makes the page uncacheable for zero benefit.
3. Storage-first for anything beyond a handful. Generate once, persist, serve many:
public function issueTicket(Ticket $ticket): void
{
$path = "qr/{$ticket->uuid}.png";
Storage::disk('s3')->put($path, $this->qr->png($ticket->signedUrl()));
$ticket->update(['qr_path' => $path]);
}
Serve from S3/CloudFront. This is the only pattern that scales — GD PNG encoding dominates wall time once you're above a few codes per request.
4. Cache keyed on the actual inputs. Output is a pure function of (payload, ec_level, size, margin). That tuple is your cache key:
$key = 'qr:' . hash('xxh3', "{$payload}|{$ec}|{$size}|{$margin}");
return Cache::remember($key, now()->addDay(), fn () => $this->qr->png($payload));
Do not generate the same QR twice. Pre-generate static codes (marketing landing pages, packaging inserts) at build time and push them to object storage.
5. Bind it behind an interface on day one. Given the abandonment signals (6 stars, 0 dependents, dormant upstream), the highest-value pattern is not letting QRcode::png() leak into controllers. One interface, one adapter, and swapping to bacon later is a one-file change with a contract test guarding it.
6. Batch work goes in a queue job. For invoice runs, badge printing, or warehouse labels:
foreach ($invoices->chunk(500) as $chunk) {
GenerateQrCodes::dispatch($chunk);
}
The encoder is CPU-bound PHP with a GD raster step; 500 codes in a web request will make you resent the package and the server.
7. Contract-test scannability, not pixel equality. In CI, decode generated output back with a scanner library (zxing wrapper or a PHP QR decoder) at print DPI. Scannability is what the product promises; byte-identical output is neither guaranteed across PHP versions nor preserved by any encoder swap.
8. Cap payload length at the adapter boundary. Long URLs or vCards inflate the QR version and encoding CPU non-linearly. $this->qr->png(Str::limit($payload, 1200)) at the interface is cheap insurance against a runaway 4KB vCard slowing a request from 3ms to 80ms.
Framework mismatch is the headline gotcha. Composer resolves the dependency in a Laravel app and nothing else happens. No service provider, no config publish, no Artisan command, no facade. If a teammate "installed the QR package," check whether they actually registered anything — most likely they didn't.
Global, un-namespaced classes. The upstream encoder defines QRcode, QRencode, and QRimage in the root namespace. Any other dependency that vendors phpqrcode (older WordPress plugins, legacy internal libraries) will collide on "cannot redeclare class QRcode" — but only when both autoload in the same request, so it will surface in production, not CI. If you keep the package, fork and namespace it; don't paper over it with class_exists guards.
GD is hard, not soft. Missing extension → fatal error, not exception. Fail at boot:
if (!extension_loaded('gd')) {
throw new \RuntimeException('QR generation requires ext-gd');
}
Put this in a service provider's register() so a misconfigured container fails on deploy, not on the first user who hits a ticket page.
Silent file-write failures. Unwritable target directories can produce zero-byte PNGs that Browsers happily display as broken images but scanners reject. Validate the target directory at deploy time and assert strlen($png) > 0 before persisting. Same story for temp paths — some encoders write to /tmp silently and return the bytes; if /tmp isn't writable you get empty output with no error.
Config quirks. The Symfony config node may silently ignore keys you copy from the README if they aren't declared in Configuration.php (or it will throw an unhelpful "unrecognized option" error depending on the node's ignoreExtraKeys setting). The authoritative list of keys is in the extension class, not the documentation. Also confirm the type of each — an int-vs-string mismatch on error correction is a silent fallback to a default.
Quiet zone / margin. The default is often below the 4-module quiet zone the barcode spec calls for. Codes that scan on a retina screen will fail on printed paper under a handheld scanner in dim light. If anything will be printed, bump the margin and test at actual print size and DPI — a product-visible defect, not a crash.
Error correction choice. M (15%) is a sane default. H (30%) if the code will bear a logo overlay, get folded, or be photographed at an angle. L almost never justifies the reduced robustness — payloads are small enough that the size cost is irrelevant.
Encoding edge cases. Legacy phpqrcode accepts UTF-8 bytes but doesn't strictly enforce ECI declaration, so a code generating successfully can still fail to scan if your payload has accented characters, emoji, or a CJK name. Test with a real scanner, or percent-encode non-ASCII at the adapter boundary and document why.
PHP version churn is a regression event. The upstream codebase predates PHP 8's stricter deprecations. Treat every minor PHP bump as a reason to re-run a smoke test (generate → decode → assert) on the target runtime. Don't let this land on a shared "latest PHP" CI image where a PHP bump silently breaks generation on the day a shared base image updates.
License stack. The bundle is MIT; the wrapped encoder is LGPL-3.0. Fine for a server-side SaaS where you're not distributing code. If your shipping model includes on-prem distribution, a Docker image you hand to customers, or a desktop bundle, review obligations before this becomes a shipping decision.
Extension point / exit strategy. The only extension point worth using is the one you build: a QrCodeGenerator interface, a single adapter, a scannability contract test, and a documented owner. That converts the package from a dependency you're stuck with into a swap you can execute in one PR — which, given the maintenance posture (no issue triage, no security advisories, no downstream canaries), is the real day-to-day leverage.
How can I help you explore Laravel packages today?