libern/qr-code-reader
A PHP/Laravel library that reads and decodes QR codes from image files. Its simple API returns the decoded text, making it easy to scan uploaded or stored QR images in your app. Includes Laravel service provider integration and supports common image formats.
Install the package
composer require libern/qr-code-reader
The package is a thin wrapper around a decoder binary/jar, so installation is only half the job — the runtime prerequisites matter more than the Composer step.
Verify the runtime prerequisites before writing any code
exec()/shell_exec() must not be in disable_functions.java -version. In Docker add default-jre-headless (Debian) or openjdk11-jre-headless (Alpine).vendor/libern/qr-code-reader — deploy scripts that prune non-PHP files will break it.Look here first
src/QRCodeReader.php — the decode() method and how it builds the shell command. This tells you exactly what path format, quoting, and return values you get.src/QRCodeReaderServiceProvider.php — whether it binds a singleton and whether auto-discovery (extra.laravel.providers) is set.src/Facades/ — if a facade is shipped, alias it; otherwise resolve from the container.First use case — decode an uploaded image
use Libern\QRCodeReader\QRCodeReader;
$reader = app(QRCodeReader::class);
$text = $reader->decode(storage_path('app/qr/inbound.png'));
// Treat empty/null as "no QR found" — the package does not throw on failure.
if (blank($text)) {
abort(422, 'No QR code detected.');
}
Smoke-test with a known fixture before building features: generate a QR with simplesoftwareio/simple-qrcode or endroid/qr-code, decode it, assert round-trip. This immediately proves Java/exec/permissions are working.
Wrap it in your own interface. The package exposes a concrete class, and you will want to swap it eventually (see Gotchas). Define App\Contracts\QrDecoder with decode(string $absolutePath): ?string and a ZXingQrDecoder adapter that delegates to QRCodeReader. Bind it in a service provider. Every call site then depends on your contract, not Libern\....
// AppServiceProvider::register()
$this->app->bind(QrDecoder::class, ZXingQrDecoder::class);
// ZXingQrDecoder
public function decode(string $path): ?string
{
$result = trim((string) app(QRCodeReader::class)->decode($path));
return $result === '' ? null : $result;
}
Controller flow for uploads. Validate (mimes:png,jpg,jpeg,gif, max:5120) → store on a private disk → decode by absolute path → unlink in finally. Never pass the client's original filename into the decoder.
public function store(DecodeQrRequest $request, QrDecoder $decoder)
{
$path = $request->file('image')->store('qr/tmp', 'local');
$absolute = Storage::disk('local')->path($path);
try {
$text = $decoder->decode($absolute);
return $text ? response()->json(['text' => $text])
: response()->json(['error' => 'not found'], 422);
} finally {
Storage::disk('local')->delete($path);
}
}
Push decoding to a queue. Each invocation spawns a process (and a JVM if Java-backed), costing hundreds of milliseconds. For user uploads or bulk imports, dispatch a DecodeQrCode job, then broadcast/notify. This keeps request latency flat and gives you retries and failure logging for free.
class DecodeQrCode implements ShouldQueue
{
public int $tries = 2;
public function __construct(public string $diskPath) {}
public function handle(QrDecoder $decoder): void
{
$text = $decoder->decode(Storage::disk('local')->path($this->diskPath));
QrDecoded::dispatch($this->diskPath, $text);
}
}
Batch / artisan command. A php artisan qr:decode {path} command is invaluable for ops debugging and for importing a folder of images. Process files one by one and log failures rather than aborting the run.
Cache by content hash. sha1_file() → cache the decoded payload for an hour. Re-scanning the same uploaded asset (e.g., in a retry) then costs zero processes.
Temporary files for base64/data-URI payloads. Write bytes to tempnam(sys_get_temp_dir(), 'qr_'), decode, delete. Don't try to feed the decoder a data URI.
Round-trip testing. Use a generation library in tests to produce a QR, decode it, assert equality — this catches environment regressions (Java removed from CI image) that a mocked test would hide. Keep a checked-in fixture image for a fast unit-level test too.
exec, and slim containers omit a JRE. Both fail silently — you get an empty string, not an exception. Add a health check and fail loudly at boot in non-production.decode() returning empty could mean "no QR", "bad path", "java not found", or "non-zero exit". When debugging, reproduce the underlying command manually (java -jar … image.png) to separate package issues from environment issues. Wrap the call with your own logging of the raw output.storage before decoding.-Dfile.encoding=UTF-8 flag on the Java side). Test with an accented/emoji payload, not just https://example.com.Storage::disk()->path() or storage_path().vendor/libern/qr-code-reader during deploy, and why a fork is sometimes easier than fighting the path.composer.json, run it against your PHP version in CI (PHP 8.1+ may surface deprecations), and keep the adapter layer so you can move to khanamiryan/qrcode-detector-decoder (pure PHP, ext-gd, no Java) or a hosted decoding API without touching call sites.redirect($text) directly.QrDecoder, alias the facade to your adapter, and optionally add a QrDecoder fake in tests so feature tests don't require a JRE at all.How can I help you explore Laravel packages today?