lara-zeus/simple-qrcode
A simple Laravel package for generating QR codes with minimal setup. Create scannable codes for URLs, text, and other data directly in Blade views or PHP code, making it easy to add QR functionality to your application.
Install and verify before you write a feature.
composer require lara-zeus/simple-qrcode
composer show --tree lara-zeus/simple-qrcode
There is no meaningful changelog (v5.0.0 reads "Starting…"), so your first ten minutes should be spent reading the source rather than the README. Look at, in order:
vendor/lara-zeus/simple-qrcode/composer.json — see what encoder it pins underneath (bacon/bacon-qr-code or a vendored backend) and whether ext-gd/ext-imagick is declared.src/*ServiceProvider.php — confirm whether it auto-discovers, what config key it publishes, and which facade accessor it registers. This is the collision hazard if you already have simplesoftwareio/simple-qrcode installed.src/Facades/* and the Blade component class — that's the real API surface. The description promises a fluent API and a Blade component; the docs don't document either, so the class is the spec.Confirm the extension is actually present in the image you'll deploy to:
php -m | grep -Ei 'gd|imagick'
Then hit it from Tinker to prove the whole chain works before building anything on it:
// accessor + method names: confirm against src/Facades
echo QrCode::size(300)->generate('https://example.com');
First use case: an invoice or ticket PDF. It's the lowest-risk surface, it's server-side by nature, and it immediately proves the thing you actually care about — SVG/PNG output inside a PDF pipeline — without touching a browser or an email client. Render one SVG, look at it on screen, scan it with your phone, then wire it into the PDF and stop. That's the spike; it should take half a day including the reading.
Wrap it on day one. This is a leaf dependency with 0 dependents and a one-team bus factor. The wrapper is the single highest-leverage thing you'll write here — it turns "is this package abandoned?" into a one-file change.
namespace App\Support\Qr;
interface QrRenderer
{
public function svg(string $payload, int $size = 300): string;
public function dataUri(string $payload, int $size = 300): string;
public function png(string $payload, int $size = 300): string;
}
namespace App\Support\Qr;
use LaraZeus\SimpleQrCode\Facades\QrCode; // confirm accessor
class LaraZeusQrRenderer implements QrRenderer
{
public function svg(string $payload, int $size = 300): string
{
return QrCode::format('svg')
->size($size)
->errorCorrection('M')
->generate($payload);
}
public function dataUri(string $payload, int $size = 300): string
{
return 'data:image/svg+xml;base64,' . base64_encode($this->svg($payload, $size));
}
public function png(string $payload, int $size = 300): string
{
return QrCode::format('png')
->size($size)
->errorCorrection('M')
->generate($payload);
}
}
Bind the interface in a service provider and inject it — never call the facade from a controller or a Blade view.
Pick the format at the boundary, not per call site. SVG for anything going to print or into a PDF (crisp at any size, tiny payload). PNG for email and for anything a browser or an ageing scanner has to cope with. Encode that decision once:
public function forPdf(string $payload): string
{
return $this->qr->svg($payload, 400);
}
public function forEmail(string $payload): string
{
return $this->qr->dataUri($payload, 200); // raster, not SVG
}
A cached route endpoint beats inline rendering. If a view or an email needs a QR image URL, serve it rather than regenerating on every request:
Route::get('/qr/{code}', QrController::class)
->middleware('throttle:60,1')
->name('qr.show');
public function __invoke(string $code)
{
$payload = Cache::rememberForever("qr:$code", fn () => $this->resolvePayload($code));
return response($this->qr->png($payload, 400), 200, [
'Content-Type' => 'image/png',
'Cache-Control' => 'public, max-age=31536000, immutable',
'ETag' => '"' . md5($payload) . '"',
]);
}
Key the URL on the entity, not the payload — /qr/invoice/4412, resolved server-side. Otherwise your public endpoint is an open redirect-shaped payload encoder for anyone who can craft a URL.
Sign the payload, not the image. QR codes are a plaintext transport. If a scanned code grants anything, put a signed token in it and verify on scan:
$payload = URL::temporarySignedRoute('checkin.show', now()->addDays(30), ['ticket' => $ticket->id]);
Payload builders are plain PHP — keep them testable and separate. vCard, Wi-Fi handoff, and TOTP provisioning URIs all have their own escaping rules, and every one of them is a place where a stray ; or unescaped newline produces a QR code that scans into garbage:
final class VCard
{
public static function make(Contact $c): string
{
return implode("\r\n", [
'BEGIN:VCARD', 'VERSION:3.0',
'FN:' . self::escape($c->name),
'TEL;TYPE=WORK,VOICE:' . self::escape($c->phone),
'END:VCARD',
]);
}
}
Error correction is a size trade-off, not a quality slider. M is the right default. Use H for codes printed small or expected to be physically abused (packaging, outdoor signage) and accept that the matrix gets denser and you must print it larger. Don't reach for H reflexively.
Test the wrapper, not the encoder. Assert on things that survive an encoder bump:
it('renders an svg for an invoice', function () {
$svg = app(QrRenderer::class)->svg('https://example.com/pay/1');
expect($svg)->toStartWith('<svg')
->and(substr_count($svg, '<rect'))->toBeGreaterThan(10);
});
it('returns a png data uri', function () {
$png = base64_decode(explode(',', app(QrRenderer::class)->dataUri('x'))[1]);
expect(substr($png, 0, 8))->toBe("\x89PNG\r\n\x1a\n");
});
Capture one golden SVG before you upgrade versions and diff it after — but don't assert on exact bytes in CI forever; mask-pattern selection can legitimately change between encoder versions.
Batch jobs are the real win. Labels, ticket runs, statement enclosures — generate once, store, reference. A hundred-ticket PDF should render a hundred QR codes in one pass, not hit a route a hundred times.
Check for a facade collision before you install. If simplesoftwareio/simple-qrcode (or anything else) is already in composer.json and both register a QrCode accessor, service-provider ordering determines which one you get — and it can flip nondeterministically after an unrelated composer update. If namespaces collide, cut over in a single commit; do not attempt coexistence.
Pin exactly ("lara-zeus/simple-qrcode": "5.0.0"), not ^5.0. With a single release and no dependents there's no semver track record to trust. Add a weekly composer update --dry-run CI job so an upstream encoder advisory surfaces as a resolvable bump rather than silent drift.
ext-gd is the classic deploy failure. Slim php:8.x-cli and Alpine images routinely ship without it. If the package doesn't declare it in composer.json, add a boot-time health check so it fails at deploy rather than on the first customer scan:
// AppServiceProvider::boot()
if (! extension_loaded('gd') && ! extension_loaded('imagick')) {
throw new RuntimeException('QR rendering requires ext-gd or ext-imagick.');
}
Read-only / serverless filesystems. Verify the package writes to memory or a stream rather than a local temp path. If it doesn't, you'll find out in the Lambda-equivalent environment, not locally.
Don't put SVG into email. Gmail, Outlook, and most clients strip or refuse it. Use PNG. And watch data-URI length — large PNGs inline into an email body will get clipped by some clients; if the image is big, attach it or host it, don't inline it.
Transparency and contrast are the top cause of "it won't scan." A QR on a transparent background composited onto a dark card is invisible to a scanner. Always render with an explicit light background and dark foreground, keep the contrast ratio high, and verify the quiet zone (the blank margin) survives your CSS or PDF layout — cropping the margin is the second most common failure.
Respect payload capacity. Byte mode tops out around 2,953 bytes and shrinks as error correction rises. Long signed URLs are the usual culprit. Use short opaque tokens (/q/9f3a) and resolve server-side rather than signing a long query string into the code.
A QR code is not a security boundary. It is plaintext that anyone can read. Never encode a secret you wouldn't paste into a public URL — no raw API keys, no unhashed tokens, no PII beyond what the feature needs. For TOTP provisioning URIs, treat the code as equivalent to the shared secret and scope its lifetime accordingly.
Sanitize user-supplied payloads. If a tenant can supply the string that becomes a QR code, you're encoding arbitrary content into an image your users will scan. Validate the scheme (https only for URLs), reject javascript: and data:, and build vCard/Wi-Fi payloads through escaping helpers rather than string concatenation.
Config quirks are undocumented — publish and diff. Publish the config tag, then diff it against the package default. If the config file doesn't exist, hardcode your settings in the wrapper instead of reaching for config() keys you're guessing at, and revisit if a config lands in a later release.
Name an owner for the fork. Zero dependents means no early-warning system: no CVE triage, no issue corpus, no Stack Overflow safety net. Put a name in the ADR before you depend on it, keep the wrapper interface narrow so replacement is a one-file change, and if you ever need more than the wrapper gives you, bacon/bacon-qr-code is the maintained encoder underneath most of this ecosystem — that's your exit path, and the wrapper is what makes taking it cheap.
How can I help you explore Laravel packages today?