zendframework/zend-barcode
Zend Barcode is a standalone PHP component from Zend Framework for generating barcodes. It supports common symbologies such as Code 39, Code 128, EAN, UPC, QR Code, and PDF417, with renderers for images and PDF, plus validation and object-oriented configuration.
Install (and read the abandoned warning first). zendframework/zend-barcode was archived when Zend Framework became Laminas; the last tag is 2.8.0 (2019-12-26) and Composer marks it abandoned. It still installs, but its composer.json pins php: ^5.6 || ^7.0, so on PHP 8.x you'll need --ignore-platform-req=php or you'll hit a resolution wall. For anything new, install the maintained fork and swap namespaces — the API is byte-for-byte identical:
composer require laminas/laminas-barcode # Zend\Barcode\* -> Laminas\Barcode\*
You need the GD extension (ext-gd) for the image path; the PDF renderer additionally wants the long-dead zendframework/zendpdf, which is why most teams never use it.
Minimal path to a barcode. The whole component is two objects: an Object (the symbology) and a Renderer (the output format). Barcode::factory() wires them:
use Zend\Barcode\Barcode;
$renderer = Barcode::factory(
'code128', // Object
'image', // Renderer
['text' => 'ORD-2024-000123', 'drawText' => true, 'font' => public_path('fonts/DejaVuSans.ttf')],
['imageType' => 'png', 'width' => 400, 'height' => 120,
'horizontalPosition' => 'center', 'verticalPosition' => 'middle']
);
$renderer->draw(); // builds the GD resource
$gd = $renderer->getImage(); // GD resource
ob_start(); imagepng($gd); $png = ob_get_clean();
return response($png, 200, ['Content-Type' => 'image/png', 'Cache-Control' => 'public, max-age=31536000']);
Where to look first. src/Barcode.php for factory() and the type→class map; src/Object/* for the symbologies and their option setters (Code128, Ean13, QrCode, Pdf417, Itf14, …); src/Renderer/* for Image, Svg, Pdf, Html. The options array you pass to factory() is just sugar for the setters on those two classes — when an option name isn't obvious, read the class.
First use case. A packing-slip / order-lookup barcode: code128 with the order number, rendered to PNG in a route, <img>-ed into the slip. It's the cheapest thing to ship and immediately proves the pipeline.
Wrap it in one service, not scattered factory() calls. Options are verbose and easy to drift; centralise them.
class BarcodeService
{
public function png(string $type, string $text, array $options = []): string
{
$renderer = Barcode::factory($type, 'image',
['text' => $text, 'drawText' => true, 'font' => public_path('fonts/DejaVuSans.ttf')] + $options,
['imageType' => 'png', 'width' => 400, 'height' => 120] + $options
);
$renderer->draw();
ob_start(); imagepng($renderer->getImage());
return ob_get_clean();
}
}
Cache the bytes, always. Output is deterministic for a given type + text + options, so key on sha1($type.$text.serialize($options)) and Cache::rememberForever(), or write to storage/app/barcodes/{hash}.png and serve from there. This matters most for QR/PDF417, which are CPU-heavy relative to the other symbologies.
SVG for anything printed, PNG for anything on screen. Barcode::factory('code128', 'svg', [...])->draw() returns the SVG as a string — vector, tiny, crisp at any DPI, ideal for invoices, labels and PDF embedding. Don't reach for the Pdf renderer just to get a barcode into a PDF; render SVG (or a 600px-wide PNG) and let your PDF library place it. It avoids dragging an abandoned dependency into the app.
Batch/label sheets: build the renderer once, swap the barcode. A loop of Barcode::factory() re-parses options every iteration; instead keep the renderer and call setBarcode(Barcode::makeBarcode('code128', ['text' => $next])). You can also pass an already-constructed Object or Renderer instance as the first/second argument to factory() instead of a string.
Validate at the domain boundary, not at the renderer. Each symbology is strict: Code39 rejects lowercase, EAN/UPC want digits of an exact length and append the check digit themselves. Put a small guard (regex/length per symbology) in a FormRequest or value object so the user sees "invalid barcode" rather than a stack trace. Catch Zend\Barcode\Exception\ExceptionInterface and log the message — it's usually precise about which character is illegal.
HTML renderer is a convenience for the "I just need an <img> tag" case (it wraps the GD output). In Laravel you'll almost always prefer returning your own Response/view over letting a renderer echo markup — use it only if you're porting existing ZF view code.
automaticRenderError is true by default. An invalid symbology/text silently produces a barcode image with the error message drawn in it — which looks like a working barcode until someone scans it in the warehouse. Always pass false as the fourth/fifth argument in production paths: Barcode::factory($type, $renderer, $opts, $rendererOpts, false).render() writes headers and echoes. Calling it inside a Laravel controller after the response has started can emit "headers already sent" warnings and corrupts JSON responses. Use draw() + getImage() and return a real Response.barHeight (≈50px) unless you set height on the renderer. Scaling a 50px-tall PNG inside HTML looks mushy — render at 2–3× (or use SVG) and constrain with CSS.withQuietZones defaults on; disabling it to save pixels is a common cause of "the scanner won't read it". Same for horizontalPosition — keep some white margin around the bars.Ean13 takes 12 digits and computes the 13th; feeding it 13 gives you a wrong or rejected code. Code39 has its own optional withChecksum.drawText needs a real font. Without the font option you fall back to GD's built-in bitmap font, which is unreadable at label sizes. Ship a TTF in resources/fonts and reference it with public_path()/resource_path().QrCode and Pdf417 are drawn as bitmaps, so expect them to work with the image renderer only — the SVG path doesn't have a bar-by-bar representation of them. If you need vector QR, use a dedicated QR package (endroid/qr-code, bacon/bacon-qr-code).Zend\Barcode\Object\AbstractObject (implement validateText, getCharacters, getChecksum, draw) for a proprietary symbology, or Renderer\AbstractRenderer for a bespoke output — then hand the instance to factory(). You generally cannot register a new string type without touching the internal map, so pass the object.ext-gd to composer.json, and put a test around the one or two symbologies you use — this code is frozen, and PHP runtime changes (GD defaults, PHP 8 type strictness) will surface as silent rendering differences, not exceptions.How can I help you explore Laravel packages today?