laminas/laminas-barcode
laminas/laminas-barcode is a framework-agnostic PHP library for generating and rendering barcodes, supporting common symbologies such as Code 39, Code 128, EAN, UPC, and QR, with configurable image, PDF, and HTML renderers.
laminas/laminas-barcode is a framework-agnostic component — there is no Laravel service provider, no facade, no published config. You composer require laminas/laminas-barcode and instantiate classes directly. That is the whole integration.
composer require laminas/laminas-barcode
# ext-gd is required only for the Image renderer
php -m | grep -i gd
The two entry points you should read first:
src/Barcode.php — the static factory: Barcode::make($type, $options) and Barcode::render($type, $rendererName, $barcodeOptions, $rendererOptions).src/Object/* — one class per symbology (Code128, Code39, Code25, Ean13, Ean8, UpcA, Itf14, Codabar, Postnet, …).src/Renderer/* — Image, Svg, Html, Pdf. Pick the renderer per use case, not the symbology.First use case — a signed-internal PNG endpoint that renders a SKU as Code 128:
use Laminas\Barcode\Barcode;
use Laminas\Barcode\Renderer\Image;
Route::get('/barcodes/{sku}.png', function (string $sku) {
$barcode = Barcode::make('code128', [
'text' => $sku,
'drawText' => true,
'factor' => 2, // bar width multiplier
'barHeight' => 80,
]);
$renderer = new Image();
$renderer->setBarcode($barcode);
$renderer->setImageType('png');
// The Image renderer streams to the output buffer; capture it.
ob_start();
$renderer->render();
$png = ob_get_clean();
return response($png, 200, ['Content-Type' => 'image/png']);
})->where('sku', '[A-Za-z0-9\-]+');
The docs live at docs.laminas.dev/laminas-barcode (usage, per-symbology option tables, renderer options). Read the renderer section before the symbology section — most "it renders but scanners reject it" problems are renderer sizing, not encoding.
Wrap it in one small service, bind it as a singleton. You will call it from controllers, jobs, Blade directives, and PDF builders; duplicating the ob_start() dance in five places is how bugs creep in.
final class BarcodeService
{
public function png(string $type, string $text, array $options = []): string
{
$barcode = Barcode::make($type, $options + config('barcode.defaults') + ['text' => $text]);
$renderer = new \Laminas\Barcode\Renderer\Image();
$renderer->setBarcode($barcode);
$renderer->setImageType('png');
ob_start();
$renderer->render();
return (string) ob_get_clean();
}
public function svg(string $type, string $text, array $options = []): string
{
$renderer = new \Laminas\Barcode\Renderer\Svg();
$renderer->setBarcode(Barcode::make($type, $options + ['text' => $text]));
return $renderer->render(); // Svg returns a string
}
}
Choose the renderer by output channel, not by preference:
| Channel | Renderer | Why |
|---|---|---|
| HTTP image, print labels | Image |
Raster PNG/JPEG/GIF, works in every PDF/label tool |
| Inline in Blade, email, SVG logos | Svg |
Returns a string, scales without blur, no GD |
| HTML email without GD | Html |
Returns an HTML table of bar <div>s |
| Existing PDF pipeline | Pdf |
Only if you already use laminas/laminas-pdf |
Default to SVG for anything that leaves your server as markup, and PNG when a printer, courier API, or label template expects raster.
Cache aggressively. Barcode generation is CPU work on every request and the output is deterministic for a given (type, text, options) tuple. Hash the inputs into the filename and serve from disk/S3:
$key = 'barcodes/' . md5(json_encode([$type, $text, $options])) . '.png';
if (! Storage::exists($key)) {
Storage::put($key, $service->png($type, $text, $options));
}
return Storage::response($key, null, ['Cache-Control' => 'public, max-age=31536000, immutable']);
Validate before you render. laminas/laminas-validator ships a Barcode validator backed by the same symbology classes, so the rules (EAN-13 length, UPC checksum) match exactly. Reject with 422 instead of letting Object\Ean13 throw a 500.
$validator = new \Laminas\Validator\Barcode(['barcode' => 'EAN13']);
if (! $validator->isValid($input)) {
abort(422, implode(' ', $validator->getMessages()));
}
Batch and queue. Generating a 500-label PDF is hundreds of image encodes; do it in a queued job writing to storage, then stream a zip or attach to a mailable. Never generate inside a Blade loop that renders a table of 200 products.
Embedding in PDFs without laminas-pdf: render PNG/SVG, base64 it, and let Dompdf/Snappy handle layout. This is the path most Laravel teams take and it decouples you from the PDF renderer's dependency chain.
Extension points: implement Laminas\Barcode\Renderer\RendererInterface for a custom output target (e.g. an <img> data-URI renderer or a ZPL/EPS emitter), and extend Laminas\Barcode\Object\AbstractObject for a symbology the package doesn't ship.
Renderer\Image::render() writes to output and sends headers. It does not return a string. Always wrap in ob_start()/ob_get_clean(); calling it after any output has started produces a mangled response or "headers already sent".endroid/qr-code or simplesoftwareio/simple-qrcode — do not try to bend Code128 into a QR workflow.Laminas\Barcode\Exception\ExceptionInterface and translate to a 422 at the boundary.drawText needs a real TTF font path. Set font (or $renderer->setFont()) to an absolute path such as resource_path('fonts/DejaVuSans.ttf'). The bundled default resolves relative to the vendor tree and breaks in containerized/relocated deploys. If you don't need human-readable text, set drawText => false and sidestep fonts entirely.factor is bar width, barHeight is height — but neither guarantees scannability. Small factors plus image downscaling in CSS or PDF layout is the #1 "it renders fine but the scanner beeps" cause. Keep factor >= 2, barHeight >= 60, and preserve the quiet zone: never crop the whitespace around the barcode.stretchText, drawText, fontSize change appearance only. They never alter the encoded value or checksum. Conversely withChecksum/withChecksumInText on Code39 do change the payload — be explicit when interoperability with an existing scanner fleet matters.Html renderer emits one <div> per bar, producing very large markup for long values. Fine for a single email, bad inside a loop of 100 rows.GdImage objects, not resources. Any legacy is_resource() check, imagedestroy() cleanup, or custom code copying ZF2-era snippets will misfire.Pdf renderer depends on laminas/laminas-pdf, which is not where Laminas' active investment is. Verify its current status before adopting it; the PNG-into-Dompdf route is lower-risk.Barcode::render(...) and Barcode::make(...) differ in arity — render() takes ($type, $rendererName, $barcodeOptions, $rendererOptions). Passing renderer options as the third argument silently produces a default-sized barcode, which is a confusing debugging session.getText()/checksum. Avoid pixel-comparing PNGs — GD version differences flip a pixel and break CI.How can I help you explore Laravel packages today?