baks-dev/barcode
BaksDev Barcode is a PHP 8.4+ module for generating and reading barcodes, supporting Aztec, Code39/93/128, DataMatrix, EAN-8/13, ITF, PDF417, QR Code, UPC-A and UPC-E. Install via composer require baks-dev/barcode; requires ImageMagick and related extensions.
Reality check before you composer require: despite the Laravel-shaped assessment that floats around, baks-dev/barcode ships no documented Laravel service provider, facade, or config file. The README is a Russian-language module doc for the BaksDev ecosystem (Symfony-oriented) that exposes three CLI executables:
vendor/baks-dev/barcode/Writer/Generatevendor/baks-dev/barcode/Reader/Decodevendor/baks-dev/barcode/Pdf/PdfCropImgYour integration point as a Laravel developer is calling those binaries, not BarcodeGenerator::generate().
Check your PHP version first.
php -v # must be >= 8.4
If you're on 8.2/8.3 (the realistic state of most Laravel 10/11 apps), stop here — this package will not install. Options are upgrading, or picking endroid/qr-code for QR-only work.
Install the package.
composer require baks-dev/barcode
Install the system dependencies (Linux/Debian-style):
sudo apt-get install imagick php-imagick librsvg2-dev librsvg2-bin libcairo2-dev
sudo apt install libpoppler-glib-dev
Verify Imagick is actually loaded by PHP-FPM and the CLI SAPI:
php -m | grep imagick
Make the three binaries executable — these are not chmodded by Composer:
chmod +x vendor/baks-dev/barcode/Writer/Generate \
vendor/baks-dev/barcode/Reader/Decode \
vendor/baks-dev/barcode/Pdf/PdfCropImg
Confirm auto-discovery didn't happen. Inspect the installed composer.json:
cat vendor/baks-dev/barcode/composer.json | grep -A5 extra
If there's no laravel.providers key, you're wrapping it yourself (see next section).
vendor/baks-dev/barcode/Writer/Generate — open it and read the argument handling. This is your only source of truth for the CLI signature, since the README doesn't document it.vendor/baks-dev/barcode/Reader/Decode — same, for the decode side.php bin/phpunit --group=barcode only works inside the BaksDev monorepo, not inside your app.Given the package's CLI shape, the fastest day-one win is a route that renders a barcode on demand. Before writing the controller, run the binary once by hand to learn its arguments:
./vendor/baks-dev/barcode/Writer/Generate --help
Then wrap it:
// app/Services/BarcodeService.php
namespace App\Services;
use Symfony\Component\Process\Process;
class BarcodeService
{
public function generate(string $type, string $data, string $outPath): void
{
$bin = base_path('vendor/baks-dev/barcode/Writer/Generate');
$process = new Process([$bin, $type, $data, $outPath]);
$process->setTimeout(15);
$process->mustRun(); // throws ProcessFailedException on non-zero exit
}
}
// app/Http/Controllers/BarcodeController.php
public function show(string $type, string $sku, BarcodeService $barcode)
{
$path = storage_path('app/barcodes/'.md5($type.$sku).'.png');
if (! file_exists($path)) {
$barcode->generate($type, $sku, $path); // e.g. 'EAN-13', '4006381333931'
}
return response()->file($path, [
'Content-Type' => 'image/png',
'Cache-Control' => 'public, max-age=86400',
]);
}
Because the exact flag names aren't published, treat the first manual invocation as your integration test — capture the output and bake the real signature into the service.
Don't scatter exec() calls through controllers. A single BarcodeService gives you a seam to swap in endroid/qr-code later (see Gotchas), and a place to standardise timeouts, temp dirs, and error handling.
final class BarcodeService
{
public function __construct(private array $config) {}
public function generateToDisk(string $type, string $data, string $diskPath): string
{
Storage::makeDirectory(dirname($diskPath));
$tmp = tempnam(sys_get_temp_dir(), 'bc_').'.png';
(new Process([base_path('vendor/baks-dev/barcode/Writer/Generate'), $type, $data, $tmp]))
->setTimeout(15)
->mustRun();
Storage::put($diskPath, file_get_contents($tmp));
@unlink($tmp);
return $diskPath;
}
public function decode(string $absolutePath): string
{
$out = (new Process([base_path('vendor/baks-dev/barcode/Reader/Decode'), $absolutePath]))
->setTimeout(20)
->mustRun()
->getOutput();
return trim($out);
}
}
Barcode output is deterministic for a given (type, data) pair. Cache the file path, not the request:
public function generateCached(string $type, string $data): string
{
$path = 'barcodes/'.sha1($type.'|'.$data).'.png';
if (! Storage::exists($path)) {
$this->generateToDisk($type, $data, $path);
}
return $path;
}
This is the single biggest performance lever: rendering a PDF417 through ImageMagick costs tens to hundreds of milliseconds and real CPU, and a catalogue of 50k SKUs regenerated per request will flatten a box.
Bulk generation belongs in a job, not a request. A 5k-label print run will exceed PHP's max_execution_time long before it finishes:
final class GenerateLabelBatch implements ShouldQueue
{
public function __construct(public array $skus) {}
public function handle(BarcodeService $barcode): void
{
foreach ($this->skus as $sku) {
$barcode->generateCached('EAN-13', $sku);
}
}
}
Give these their own queue (->onQueue('barcodes')) so a slow ImageMagick box doesn't stall your mail or webhook workers.
Rather than writing to disk for one-off renders, stream from a route and let the browser cache it:
<img src="{{ route('barcode.show', ['type' => 'QRCode', 'sku' => $product->sku]) }}"
alt="Barcode for {{ $product->sku }}">
Pair with Cache-Control and an ETag derived from the payload hash so repeat scans are free.
Store the upload to a temp path first — the Decode binary wants a filesystem path, not a stream:
public function decodeUpload(Request $request, BarcodeService $barcode): JsonResponse
{
$request->validate(['image' => 'required|image|max:8192']);
$tmp = $request->file('image')->store('tmp', 'local');
$abs = Storage::disk('local')->path($tmp);
try {
return response()->json(['data' => $barcode->decode($abs)]);
} finally {
Storage::disk('local')->delete($tmp);
}
}
Validate the MIME type and cap filesize before the ImageMagick process touches it — a decompression bomb will otherwise chew through RAM.
The package is not a PDF generator. The realistic pattern splits into two halves:
Writer/Generate, then place it in your PDF with your existing tool (dompdf, Snappy/wkhtmltopdf, or FPDI). PdfCropImg is not an embedding utility.PdfCropImg input.pdf output.png renders a PDF page (or region) to an image so the Decode binary can read barcodes out of received shipping docs / invoices.Wrap the extraction step in a job — poppler rendering is heavy and a batch of inbound PDFs will block a request thread.
Give ops something they can run without a deploy:
protected $signature = 'barcode:warm {file} {--type=EAN-13}';
// reads a CSV of SKUs, calls BarcodeService::generateCached per row
Also useful: a barcode:doctor command that checks php -m | grep imagick, verifies the three binaries are executable, and exits non-zero — runnable in CI and as a deploy smoke test.
public function test_generate_then_decode_roundtrips(): void
{
$path = storage_path('app/testing/rt.png');
app(BarcodeService::class)->generate('QRCode', 'test-payload', $path);
$this->assertFileExists($path);
$this->assertSame('test-payload', app(BarcodeService::class)->decode($path));
}
Skip this test group when Imagick is absent ($this->markTestSkipped()), otherwise CI on a non-imagick runner fails for reasons unrelated to your code.
This is the single most consequential fact about the package. Laravel 10 supports PHP 8.1–8.3, Laravel 11 supports 8.2–8.4. If your app sits on anything below 8.4, Composer will refuse. Don't discover this after adding it to composer.json and wondering why composer update hangs on conflicts. Confirm php -v first, and check whether other packages in your lock file pin PHP below 8.4 — a mixed dependency graph can make the upgrade far more expensive than this package is worth.
chmod +x does not survive composer installFile modes are not preserved by Composer's archive extraction. Every fresh composer install on a new node (including your Docker build) loses the executable bit, and the binaries fail with Permission denied or sh: ...: cannot execute.
Bake it into your image/CI:
RUN chmod +x vendor/baks-dev/barcode/Writer/Generate \
vendor/baks-dev/barcode/Reader/Decode \
vendor/baks-dev/barcode/Pdf/PdfCropImg
or add a post-autoload-dump script in your composer.json. If you add it as a Composer script inside the vendor package, the next composer update wipes it.
The README documents formats and system packages, not arguments. Several published examples of this package (facades, PdfBarcode::generate(), ['width' => 300] options) are not backed by the README or any release note and may be hallucinated. Run the binaries with no arguments (or --help) and read the source of Writer/Generate before writing your service. Capture your real invocation in a test so a package update that changes the signature fails loudly rather than silently producing corrupt images.
$data is user-controlled in every realistic case (a SKU, a URL in a QR, a scanned payload). Use Symfony's Process with an argument array — it escapes each argument — rather than exec("$bin $type $data $path"). A crafted payload should not become shell syntax.
Also cap the input length: QR payload limits vary by error-correction level, and PDF417 has its own byte ceiling. Validate against the format before spawning a process, and reject anything containing non-printable bytes for 1D formats.
policy.xml will bite youDefault ImageMagick policies on many distros block PDF/PS decoding entirely:
policy domain="coder" rights="none" pattern="PDF"
Symptoms: PdfCropImg exits non-zero with a policy error that never reaches your logs because you swallowed stderr. When wrapping the process, always capture and log stderr (Symfony's ProcessFailedException carries it — don't catch and rethrow a bare RuntimeException). If you genuinely need PDF cropping, relax the policy in /etc/ImageMagick-6/policy.xml deliberately, and know you've just widened your attack surface for any user-supplied PDF.
Watch memory too: Imagick obeys memory/map limits in the same policy file, and a 300 DPI A4 raster will happily blow past a default 256 MB ceiling and get killed mid-job.
The package depends on system binaries and native ImageMagick. That rules out Vercel/Cloudflare-style runtimes and most cPanel/shared hosts. Lambda/Cloud Run works but inflates cold starts (ImageMagick + poppler + exec overhead). If you're on a container platform, the binaries must be present in the runtime stage, not just the build stage.
Zero stars and zero dependents on a package that shells out to three executables is a maintenance risk, not a style objection. Defend against it:
BarCodeRenderer contract in your app, BaksDevRenderer implementation. Nothing else in your codebase references the vendor path.endroid/qr-code is pure-PHP, no Imagick, handles QR/easy formats and can serve as the QRCode implementation behind the same interface if the binaries break on a deploy. It won't cover PDF417/EAN — those stay on BaksDev, or you evaluate zint."baks-dev/barcode": "7.4.8" rather than ^7.4. Release notes for 7.4.6/7.4.7/7.4.8 are literally just "v7.4.8" — you have no changelog to diff against, so an unattended minor bump is an unattended regression.composer update, you know instantly.EAN-13, EAN-8, UPC-A, UPC-E, and ITF all carry a mod-10 check digit. Passing a bad digit to the generator produces either an error from the binary or, worse, a scannable-format image that retail scanners will reject at the till. Compute/verify the checksum in PHP first, and keep a barcode_formats config mapping format → payload regex + length so validation lives in one place:
// config/barcode.php
'EAN-13' => ['length' => 13, 'pattern' => '/^\d{13}$/', 'mod10' => true],
'QRCode' => ['max_bytes' => 2953, 'pattern' => '/^[\s\S]+$/'],
tempnam() returns a path; the binary will overwrite it without complaint, but a failed write leaves junk in /tmp. Use try/finally around the unlink, and give the temp dir an explicit prefix (bc_) so you can rm /tmp/bc_* in a cleanup cron when a long-running worker leaves orphans behind.
How can I help you explore Laravel packages today?