amirezaeb/heroqr
HeroQR is a Laravel package for generating customizable QR codes—URLs, text, email, phone, WiFi, and more—with colors, logos, and multiple output formats. A simple fluent API and ready-to-use helpers make adding scannable codes to any Laravel app fast and easy.
Install and publish
composer require <vendor>/<qr-package> # check the README for the exact constraint
php artisan vendor:publish --tag=qr-config
Pin the version ("^2.1") — the 2.x line is additive so far, and styling options keep landing in minors.
Look here first, in this order
config/qr.php (or whichever tag the package publishes) — this is where default size, margin, error-correction level, format (svg/png), and the new style keys live. Read it before writing any code; most "why does my QR look wrong" questions are answered in this file.README customization section — v2.1 expanded the examples here specifically for cursor/marker styling, so it's now the best source of copy-pasteable style arrays.tests/ directory — the fastest way to see which methods are public API and which are internal, and to learn the exact option names for the new cursor/marker styles without reverse-engineering the source.First use case: a controller that returns an SVG QR for a signed, expiring URL. It exercises config, the facade/helper, and response handling in about ten lines, and gives you a baseline to diff styling changes against.
Route::get('/t/{ticket}/qr', function (Ticket $ticket) {
$url = URL::temporarySignedRoute('tickets.show', now()->addDays(7), ['ticket' => $ticket]);
return response(QrCode::format('svg')->size(320)->generate($url))
->header('Content-Type', 'image/svg+xml');
});
Wrap the package, don't sprinkle it. Put generation behind one small service (App\Support\Qr\QrRenderer) that owns format, size, error correction, and your style presets. When v2.2 renames a style key, you change one file instead of forty controllers.
class QrRenderer
{
public function svg(string $payload, string $preset = 'default'): string
{
return QrCode::format('svg')
->size(config("qr.presets.$preset.size", 320))
->errorCorrection(config("qr.presets.$preset.ec", 'H'))
->style(config("qr.presets.$preset.style", []))
->generate($payload);
}
}
Drive cursor/marker styles from config, not call sites. The new styles are aesthetic knobs a designer will want to change without a deploy-adjacent code review. Define named presets in config/qr.php (default, print, dark, ticket) and reference presets by name.
Serve SVGs inline, PNGs for download. SVG is smaller, scales, and needs no GD/Imagick. Reserve format('png') for emails, PDFs, and social cards where SVG support is unreliable.
Disk + model accessor for anything persistent. Generate once at creation time, store the path, and serve the file. Regenerating per page view is pure CPU burn.
protected static function booted(): void
{
static::created(fn (Ticket $t) => $t->update([
'qr_path' => tap("qr/tickets/{$t->uuid}.svg", function ($path) use ($t) {
Storage::disk('public')->put($path, app(QrRenderer::class)->svg($t->signedUrl()));
}),
]));
}
Cache by payload and style. Any cached QR must key on hash('xxh128', $payload . json_encode($styleConfig) . $format . $size). A style change shipped in a minor release should not silently serve stale images.
Data URIs for email. Base64 the SVG into an <img src="data:image/svg+xml;base64,..."> — no public route, no auth leak, no signed-URL expiry mismatch inside a template.
Batch generation belongs in a job. A GenerateQrCodes job with Bus::batch() for event badges or fulfilment inserts; chunk so a 5,000-row import doesn't hold one worker and a memory ceiling.
Testing. Assert status, Content-Type, and that the body is non-empty and starts with <svg (or is valid PNG magic bytes). Do not assert byte equality — output changes across minor releases, and you'll be rewriting fixtures every time a style is added.
$this->get("/t/{$ticket->id}/qr")
->assertOk()
->assertHeader('Content-Type', 'image/svg+xml')
->assertSee('<svg', false);
Scannability is the real regression suite. New cursor and marker styles change the finder patterns — the three squares scanners lock onto first. Every style preset you ship needs a physical scan test on at least one iOS and one Android device, ideally at the smallest rendered size you'll actually print. A style that looks great at 320px can fail at 96px.
Raise error correction when you style. Anything that modifies or overlays the modules (logos, rounded cursors, dot markers) eats margin. Use H (~30% recovery) as the default for styled codes and drop to M/L only for plain, data-dense codes where size matters.
Contrast, not colour. Dark-on-light only, and keep the luminance delta wide. A themed brand palette applied to markers is the single most common cause of "it doesn't scan on the packaging."
Quiet zone. Styling work often tightens margins. Keep the margin at 4 modules unless you've tested otherwise; shrinking it to make a badge prettier is how you get a support ticket from a warehouse.
Extension requirements differ by format. PNG needs GD or Imagick — check php -m on the deploy target, not locally. SVG needs neither, which is another reason it's the better default. If you must use Imagick, verify it's compiled with the same delegates as your dev box.
Config caching will bite you. After editing style presets, php artisan config:cache in a deploy script — but remember any config read inside a cached closure or a queued job serialised before the change keeps the old value. Deploy workers restart after config changes.
Don't render QR codes inside Blade loops. A table of 50 rows each calling the renderer synchronously is a multi-second TTFB. Precompute and store, or lazy-load through an endpoint.
Extension points to know about: most packages in this space let you bind a custom renderer/writer or publish and override the view used for HTML output. If you need a house style that isn't in the shipped cursor/marker set, that binding is the supported seam — subclassing the facade is not.
Version pinning. v2.1 is additive (new styles, better examples), so ^2.1 is safe and there's nothing to migrate. But because style keys are still churning in minors, keep your presets in config and re-run your scan test whenever you bump.
How can I help you explore Laravel packages today?