werk365/identitydocuments
Laravel package for reading and validating identity documents. It parses MRZ data from passports and ID cards, verifies checksums, and extracts details such as names, document numbers, nationalities, birth dates, and expiry dates.
composer require werk365/identitydocuments
php artisan vendor:publish --tag=identitydocuments-config
Three things must be true before the first call succeeds:
composer require and read the constraint error before assuming it works — on Laravel 10+ you may need --ignore-platform-reqs or a fork. Check the PHP constraint resolved by Composer rather than trusting the old 7.4–8.1 range.apt-get install tesseract-ocr tesseract-ocr-eng. In Docker, add it to the image — a php:8.1-fpm base will not have it.First use case — a passport upload endpoint:
use Werk365\IdentityDocuments\IdentityDocuments;
$document = IdentityDocuments::parse($request->file('passport')->path());
$mrz = $document->getMrz(); // raw MRZ lines
// then map document number, DOB, expiry, nationality, sex off the MRZ
Where to look first, in order: the README (still the only real documentation), config/identitydocuments.php (binary paths, OCR language, temp dir), src/IdentityDocuments.php (the facade entry point and how it shells out), and the returned Document object — dd() it once and read the actual shape rather than trusting examples, because the accessors are thin and largely undocumented.
Start with a single hardcoded test image in tests/fixtures/ and get a full round trip working before touching your upload flow. Parsing a real passport is a 2–5 second operation; you want to know that early.
Controller → Job → Model. Never call parse() inline in a request. The facade blocks on a subprocess. Push it to a queued job and return a "processing" state to the UI:
ProcessPassport::dispatch($documentId)->onQueue('ocr');
// in the job
$parsed = IdentityDocuments::parse(Storage::path($doc->image_path));
$doc->update([
'document_number' => $parsed->getDocumentNumber(),
'date_of_birth' => $parsed->getDateOfBirth(),
'expiry_date' => $parsed->getExpiryDate(),
'nationality' => $parsed->getNationality(),
'mrz_valid' => $parsed->validateCheckDigits(),
]);
Crop before you OCR. Full-page OCR is the biggest source of failure. Passports have a fixed MRZ band across the bottom ~20% (TD3, two lines × 44 chars) or the bottom of the card (TD1, three lines × 30 chars). Cropping and upscaling to ~300 DPI before handing bytes to the parser dramatically improves the hit rate. If the package does not expose a crop hook, do it upstream with Intervention Image and feed the cropped file in.
Never trust the parse — validate. The MRZ carries its own check digits. Treat a failed check as "ask the user to re-upload or type it manually." Build a DocumentData DTO with confidence / needs_review flags and render an editable form pre-filled from the parse. This is the pattern that makes the package usable in production: OCR as assist, human as authority.
Manual fallback path always exists. A "enter details manually" link next to the upload is not a nice-to-have — you will hit documents it cannot read. Design the schema so manual entry writes the same columns with source = 'manual'.
Persistence: store the extracted fields in their own columns (or a JSON column) plus the raw MRZ string for audit. Delete or encrypt the source image — storing passport scans indefinitely is a GDPR liability. A common setup: encrypted S3 object, 30-day lifecycle policy, and only derived fields in MySQL.
Testing: put 3–5 fixture images in tests/fixtures/ (one clean passport, one ID card, one blurry failure case) and assert the parsed output. Because parsing shells out to a binary, mark these tests with a group (@group ocr) so CI without Tesseract can skip them, and mock the facade for the rest of your suite.
GPL-3.0 is the headline risk. This license is copyleft and viral. If you are shipping a closed-source SaaS or a distributed Laravel app, linking this package is legally murky at best. For commercial work, budget time to evaluate alternatives (robertprice/mrz-parser-style pure parsers, cloud APIs like Mindee/Regula/AWS Textract) or plan to isolate it behind a service boundary you can swap. Check composer show werk365/identitydocuments for the license field before you build on it — 2.0.5 did not change it.
2.0.5 is a maintenance release, not a revival. The only note is "Laravel 9 support", and it landed as a patch bump — so expect no API changes and no breaking behaviour, just a widened composer.json constraint. That is genuinely useful if you are pinned to Laravel 9 (it removes the old --ignore-platform-reqs workaround), but it says nothing about Laravel 10/11, PHP 8.2+, or the OCR core. Treat the dependency as low-velocity: pin the version you validate against, and fork into your own namespace if you adopt it — you will still end up owning the maintenance for anything beyond Laravel 9.
The binary is the actual dependency. A Class '...' not found is easy; a sh: tesseract: not found inside a container is the one that eats an afternoon. Add tesseract-ocr to every environment image including CI, and make the path configurable rather than absolute.
Config caching. If you change the Tesseract path or language and nothing happens, php artisan config:cache with stale values is the culprit — or rather, not running php artisan config:clear. Same class of bug as every Laravel package config. Re-run vendor:publish --force only deliberately; the 2.0.5 upgrade should not have added config keys, but diff the published file after any bump.
Raw text is your debugger. When parsing fails, dump the OCR output before it reaches the MRZ parser. Nine times out of ten you'll see garbled characters from glare, skew, or a low-res phone photo — an image problem, not a parser problem. Build a small artisan command (php artisan id:debug path/to.jpg) that prints the raw OCR text and the parsed attempt side by side.
MRZ format variance. TD1 (ID cards), TD2, and TD3 (passports) have different line counts and field offsets. Confirm the package handles the document types you actually receive — many implementations assume TD3 only and silently produce garbage on national ID cards.
Extension points are thin. There's no documented driver abstraction for swapping OCR engines or parsers, and 2.0.5 adds none. If you need to change behaviour, expect to fork and edit src/ directly. Keep your own calls behind a small wrapper service (App\Services\DocumentParser) so the package is confined to one file when you migrate off it.
How can I help you explore Laravel packages today?