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

Qrcode Laravel Package

endroid/qrcode

Endroid QR Code is a widely used PHP library for generating QR codes, with Laravel integration via a service provider and facade. Output PNG, SVG, EPS, or binary, with configurable size, colors, margins, error correction, and logo embedding—no external APIs needed.

Deep Wiki
Context7

Getting Started

Install and verify the environment first. The package is small but has two hard runtime dependencies you should confirm before writing any code:

composer require endroid/qrcode
php -m | grep -i gd        # required for PNG/WebP output

SVG output needs no image extension, so if ext-gd is unavailable in a container, you can still ship SVG.

First use case — an endpoint that returns a QR code:

use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Writer\PngWriter;

Route::get('/qr/{text}', function (string $text) {
    $result = new Builder(
        writer: new PngWriter(),
        data: $text,
        encoding: new Encoding('UTF-8'),
        errorCorrectionLevel: ErrorCorrectionLevel::Medium,
        size: 300,
        margin: 10,
    )->build();

    return response($result->getString(), 200)
        ->header('Content-Type', $result->getMimeType());
});

Where to look first, in this order:

  1. Endroid\QrCode\Builder\Builder — the single entry point in v5. It is a plain object with named constructor parameters and a build() method. Read its constructor signature; it is the de facto configuration schema.
  2. Endroid\QrCode\Writer\WriterInterface — one method, write(QrCodeInterface $qrCode): ResultInterface. Available writers: PngWriter, WebPWriter, SvgWriter, EpsWriter, BinaryWriter, DebugWriter, and ValidatingWriter (a decorator).
  3. Endroid\QrCode\Writer\Result\ResultInterface — what you actually hand back to the caller: getString(), getDataUri(), getMimeType(), getWidth(), getHeight(), saveToFile().
  4. The value objects Encoding, ErrorCorrectionLevel, RoundBlockSizeMode, Color, Label, Logo. In v5 the first three are PHP enums, which means invalid values fail at parse time rather than at render time.

If you are coming from v4 examples on the internet, be aware the fluent Builder::create()->writer(...)->build() style and $qrCode->writeString() are gone or deprecated; v5 expects immutable config objects.

Laravel integration point: wrap the builder in a small service or a QrCode::make() helper rather than calling new Builder(...) from controllers — you will want one place to change size, margin, and error correction for the whole app.

Implementation Patterns

Config-driven factory

Put the knobs in config/qrcode.php and resolve them once:

// app/Services/QrCodeService.php
final class QrCodeService
{
    public function __construct(private array $config) {}

    public function png(string $data, ?int $size = null): ResultInterface
    {
        return new Builder(
            writer: new PngWriter(),
            data: $data,
            encoding: new Encoding('UTF-8'),
            errorCorrectionLevel: ErrorCorrectionLevel::from($this->config['ecc']),
            size: $size ?? $this->config['size'],
            margin: $this->config['margin'],
            roundBlockSizeMode: RoundBlockSizeMode::Margin,
            foregroundColor: new Color(...$this->config['fg']),
            backgroundColor: new Color(...$this->config['bg']),
        )->build();
    }
}

Bind it in a service provider with config('qrcode'). This keeps payloads out of your controllers and makes the output consistent across tickets, invoices, and 2FA enrollments.

Streaming vs. data URI vs. file

  • Stream when the QR is the response body (download, API endpoint, print-ready PDF embed). Use $result->getString() with Content-Type: $result->getMimeType().
  • Data URI when embedding in a Blade email or an <img>: $result->getDataUri() returns a complete data:image/png;base64,... string. Great for mailables, but it bloats the message — keep payloads short.
  • File when the code is persisted, e.g. attaching a QR to an invoice PDF: $result->saveToFile(storage_path("app/qr/{$order->id}.png")). The directory must already exist.

Inline in Blade

<img src="{{ $qrDataUri }}" alt="Scan to pay" width="220" height="220">

Do this in the controller/mailable, not in the template — you do not want to instantiate a builder during view rendering for every partial.

Choose the writer per channel

Channel Writer Why
Web UI, retina, print SvgWriter Resolution-independent, tiny, no GD dependency
Email, legacy clients, PNG-only consumers PngWriter Universally supported
Modern web, file size matters WebPWriter Smaller than PNG, decent support
PostScript / print pipeline EpsWriter Vector for pre-press
Raw bit matrix BinaryWriter For custom rendering or tests

For SVG, size affects the viewBox, not the rendered pixel size — your CSS controls final dimensions.

Payload builders

Treat the payload as a first-class concern. Don't concatenate URLs in five places; build a canonical, stable string (it is also your cache key):

$payload = route('tickets.show', ['ticket' => $ticket->uuid, 'sig' => $ticket->signature]);

Stable payloads mean you can cache aggressively.

Caching

QR generation is cheap but not free for large batches. Cache on a hash of the entire config, not just the payload:

$key = 'qr:'.hash('xxh128', serialize([$payload, $size, $ecc, $fg, $bg, $writer]));
return Cache::remember($key, now()->addDay(), fn () => $service->png($payload));

Cache the binary string (or write to disk and cache the path). Never cache the ResultInterface object across requests in something like Redis — serialize the bytes.

Decorating a writer

Writers are stateless and composable. A decorator is the idiomatic extension point — e.g. logging generation time, asserting the output is decodable in non-production, or adding a quiet-zone watermark:

final class TimedWriter implements WriterInterface
{
    public function __construct(private WriterInterface $inner) {}

    public function write(QrCodeInterface $qrCode): ResultInterface
    {
        $start = microtime(true);
        $result = $this->inner->write($qrCode);
        Log::debug('qr.generated', ['ms' => (microtime(true) - $start) * 1000]);
        return $result;
    }
}

ValidatingWriter follows the same pattern — wrap your real writer with it in tests to fail fast on unreadable output.

Testing

Assert on getMimeType() and on the fact that a decode of the output yields the original payload (use a QR decoder like khanamiryan/qrcode-detector-decoder in a test-only dependency). Snapshotting the raw bytes is fragile — a dependency bump will churn every fixture.

Gotchas and Tips

ext-gd is not optional for raster. PngWriter and WebPWriter will throw a clear but late error if GD is missing. If your production image is slim, either add gd (with freetype for labels) or standardise on SvgWriter.

The quiet zone is part of the spec, not decoration. margin defaults to 10 modules and scanners rely on it. Don't set margin: 0 to "save space" — many phone scanners will fail on a QR flush against a dark background. Similarly, RoundBlockSizeMode::Margin (rounding the outer size up to whole modules) is safer than shrinking module size; a QR rendered at 199px from a 25-module grid produces fractional pixels per module and degrades scanning on cheap cameras.

Error correction is a trade-off, not a "max it out" setting. High survives a logo or smudges but increases module density for the same payload and physical size, which can hurt scanning if the print is small. Medium is the sane default; Quartile/High only when you overlay a Logo.

Logos need punchoutBackground: true and a resizeToWidth no larger than roughly 20–25% of the total size. A logo without punchout leaves the previous modules underneath and looks broken; a logo too large destroys the error-correction budget.

logo: new Logo(path: public_path('img/logo.png'), resizeToWidth: 60, punchoutBackground: true),

The logo path must resolve at runtime on the machine rendering the code — public_path() is fine locally, but if you render QR codes in a queue worker or a Lambda, verify the file is deployed there too.

Labels need a real TTF file and freetype. new Label(text: 'Scan me', fontPath: base_path('resources/fonts/Inter.ttf'), fontSize: 14, ...) — a missing font path silently produces a QR without a label in some versions rather than throwing. Assert once in a test that getHeight() grew after adding a label.

SVG ignores most raster options. Foreground/background colors work; roundBlockSizeMode and anything pixel-based is meaningless. If you switch writers by channel, keep per-writer config rather than one shared array.

saveToFile() does not create directories. Use File::ensureDirectoryExists() first, and write to a temp file then rename() if multiple workers may write the same path.

Payload size is hard-limited by capacity. Long URLs with long signatures can throw an "data too long" exception. Shorten with a redirect/token table rather than dropping to Low error correction — a dense QR is worse than an extra HTTP hop.

Encoding matters for non-ASCII. Without encoding: new Encoding('UTF-8'), payloads containing accented characters, CJK, or emoji produce unreadable codes because the byte mode differs from what the scanner assumes.

Builder is not a singleton you mutate. In v5 the builder takes configuration at construction. If you find yourself writing $builder->setSize(...) you are following a v4 tutorial; instantiate a new builder or extract a factory. Constructing one is cheap — the writer does the real work.

BuilderRegistry is the DI hook. If you register named builders there, you can inject differently-configured builders (e.g. ticket-qr, invoice-qr) instead of branching on a config array at every call site.

Don't return getString() from a JSON API. Base64-encode it or return getDataUri(); raw PNG bytes inside a JSON string will be corrupted by encoding unless base64'd, and you'll spend an hour wondering why the image is "almost right."

Add explicit cache headers to streaming endpoints (Cache-Control: public, max-age=86400) keyed by payload — otherwise every page load regenerates the same QR, and in a ticket-listing view that's N renders per request.

Debugging unreadable output: swap in DebugWriter to get the matrix plus rendered output, and wrap with ValidatingWriter in tests. Nine times out of ten the cause is insufficient margin, too-small size, or a foreground/background pair with < 4.5:1 contrast — inverted colors (light modules on dark) fail on many scanners despite being technically valid.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings