Skip to content

v2.0.0 — PHP 8.2, check-digit validation, typed API, and new formats#17

Merged
rakibdevs merged 25 commits into
mainfrom
hotfix/upgrade
Jul 1, 2026
Merged

v2.0.0 — PHP 8.2, check-digit validation, typed API, and new formats#17
rakibdevs merged 25 commits into
mainfrom
hotfix/upgrade

Conversation

@rakibdevs

Copy link
Copy Markdown
Owner

Overview

Modernizes the package to PHP 8.2, fixes several correctness/robustness bugs, adds a real test suite (there was none), and introduces MRZ check-digit validation plus a typed API. This is a breaking release (PHP floor raised, new output keys), so it targets v2.0.0.

▎ ℹ️ MrzParser::parse() keeps returning the same associative array — all changes to it are additive keys. No existing key was renamed or removed.

Why

  • CI tested only PHP 8.0 and the "test suite" was a placeholder asserting true — zero real coverage.
  • The parser extracted data but never verified MRZ check digits, so corrupt/typo'd input was returned silently as valid.
  • Real-world input broke it: \r\n (Windows/OCR) inflated line length and failed validation; impossible dates (e.g. Feb 30) silently rolled over; a date of birth could resolve to the future.
  • A PHP 8.4 implicit-nullable deprecation was already present.

Breaking changes

  • Minimum PHP is now 8.2 (was 8.0).
  • parse() returns additional keys: issuer_code, nationality_code, sex, optional_data, valid, validation, raw.
  • Enums\DocumentType is now a backed enum instead of an abstract class with constants.

Highlights

Check-digit validation

Implements the ICAO 9303 7‑3‑1 weighted checksum (Support\CheckDigit). Every result now carries an overall valid flag and a per-field validation map (document number, date of birth, date of expiry, personal number, and composite). Verified against the ICAO 9303 specimens.

Typed API & ergonomics

  • MrzParser::read() → MrzResult value object (typed props, dateOfBirthAsDate(), dateOfExpiryAsDate(), isExpired(), fullName(), toArray()).
  • MrzParser::tryParse() / tryRead() → null instead of throwing on invalid input.
  • Strict mode: parse($text, strict: true) throws InvalidChecksumException when a check digit fails.

New formats & helpers

  • French national ID (legacy 2×36 Carte Nationale d'Identité) — bespoke layout, no expiry encoded. Offsets independently corroborated against an authoritative parser; the specimen's three check digits (incl. the distinctive continuous cross-line composite) all validate.
  • Name transliteration (Support\Transliterator): fromLatin('Müller') → 'MUELLER' and matches() to compare a visual-zone name against an MRZ name.

Robustness fixes

  • Central input normalization (MrzNormalizer): CRLF/CR → LF, trim, drop blank lines — Windows/OCR input now parses.
  • Strict date validation (rejects impossible dates); date of birth is century-corrected to the past.
  • Fixed the PHP 8.4 implicit-nullable deprecation and guarded against substr(null, …).
  • Consistent trailing-filler trimming for given names across all document types.

Modernization

declare(strict_types=1) throughout, typed properties, match-based adapter selection, cached country lookup table.

Laravel

Auto-discovered MrzParserServiceProvider and a ValidMrz validation rule (with optional strict mode). Requires illuminate/support (declared as a suggest + dev dependency).

Tooling & CI

  • Pest 1 → Pest 3; phpunit.xml.dist migrated to the PHPUnit 11 schema.
  • CI matrix now PHP 8.2 / 8.3 / 8.4 / 8.5; actions/checkout@v4.
  • README badges (tests, PHP version, downloads, license).

Testing

  • 99 tests / 230 assertions, ~99% line coverage (the only uncovered lines are two unreachable defensive branches).
  • Covers: every document type happy-path, validation boundaries and invalid-input cases, CRLF regression, check-digit mutation/tamper regressions, strict mode, the DTO, transliteration, and the Laravel rule.
  • composer validate ✅ · php -l clean ✅ · php-cs-fixer 0 fixes ✅ · no PHP 8.4 deprecations ✅.

Notes for reviewers

  • The French ID document number and line‑1 admin block are exposed as opaque identifiers — sources disagree on their internal sub-structure and no check digit validates it, so parsing does not depend on it.
  • Transliteration is deterministic Latin → MRZ only; the reverse is inherently ambiguous (OE may be Ö or a literal OE), so matches() covers the real use case rather than faking a lossy reverse.

rakibdevs and others added 25 commits July 1, 2026 22:53
- Bump php floor ^8.0 -> ^8.2; drop unused spatie/ray
- Upgrade pestphp/pest ^1 -> ^3, php-cs-fixer -> ^3.64, allow pest plugin
- Migrate phpunit.xml.dist to the PHPUnit 11 schema
- Test CI matrix on PHP 8.2/8.3/8.4/8.5; bump actions/checkout to v4

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump php floor ^8.0 -> ^8.2; drop unused spatie/ray
- Upgrade pestphp/pest ^1 -> ^3, php-cs-fixer -> ^3.64, allow pest plugin
- Migrate phpunit.xml.dist to the PHPUnit 11 schema
- Test CI matrix on PHP 8.2/8.3/8.4/8.5; bump actions/checkout to v4
- Convert DocumentType from abstract-class constants to a backed enum
- Add declare(strict_types=1), typed properties, and explicit return types
- Replace the adapter switch with a match keyed on the enum
- Add MrzNormalizer (CRLF/whitespace handling) used before validation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Convert DocumentType from abstract-class constants to a backed enum
- Add declare(strict_types=1), typed properties, and explicit return types
- Replace the adapter switch with a match keyed on the enum
- Add MrzNormalizer (CRLF/whitespace handling) used before validation
- DateFormatter: strict validation (reject Feb 30 etc.) and shift a future
  date of birth back a century; guard against false from createFromFormat
- GenderMapper/CountryMapper: fix PHP 8.4 implicit-nullable params (?string)
- CountryMapper: cache the merged lookup table; add mapCountryCode()
- Add NameFormatter trait with consistent trailing-filler trimming

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DateFormatter: strict validation (reject Feb 30 etc.) and shift a future
  date of birth back a century; guard against false from createFromFormat
- GenderMapper/CountryMapper: fix PHP 8.4 implicit-nullable params (?string)
- CountryMapper: cache the merged lookup table; add mapCountryCode()
- Add NameFormatter trait with consistent trailing-filler trimming
- Add Support/CheckDigit (7-3-1 weighted modulo-10 scheme)
- Every parser now returns a 'valid' flag and per-field 'validation' map
  (card_no, date_of_birth, date_of_expiry, personal_number, composite)
- Add issuer_code, nationality_code, sex, optional_data and raw keys
- Modernize parsers: typed props, NameFormatter, rtrim, normalized input

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add Support/CheckDigit (7-3-1 weighted modulo-10 scheme)
- Every parser now returns a 'valid' flag and per-field 'validation' map
  (card_no, date_of_birth, date_of_expiry, personal_number, composite)
- Add issuer_code, nationality_code, sex, optional_data and raw keys
- Modernize parsers: typed props, NameFormatter, rtrim, normalized input
- Replace the placeholder ExampleTest with real coverage (~99%)
- Happy paths per document type, invalid-input and boundary datasets
- CRLF regression, check-digit mutation, getType() branches
- DateFormatter, gender/country mappers and name edge cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the placeholder ExampleTest with real coverage (~99%)
- Happy paths per document type, invalid-input and boundary datasets
- CRLF regression, check-digit mutation, getType() branches
- DateFormatter, gender/country mappers and name edge cases
- README: PHP 8.2 requirement, new output block, check-digit and
  input-handling notes
- CHANGELOG: v2.0.0 entry (breaking changes, features, fixes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: PHP 8.2 requirement, new output block, check-digit and
  input-handling notes
- CHANGELOG: v2.0.0 entry (breaking changes, features, fixes)
- MrzResult value object via read()/tryRead(): typed props, date accessors,
  isExpired(), fullName(), toArray()
- tryParse()/tryRead() return null instead of throwing on invalid input
- strict flag on parse()/read() throws InvalidChecksumException on bad digits

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MrzResult value object via read()/tryRead(): typed props, date accessors,
  isExpired(), fullName(), toArray()
- tryParse()/tryRead() return null instead of throwing on invalid input
- strict flag on parse()/read() throws InvalidChecksumException on bad digits
- Support\Transliterator: fromLatin() (ICAO digraph/diacritic folding) and
  matches() to compare a visual-zone name against an MRZ name
- Visa optional_data now sliced by line length (16 on MRV-A, 8 on MRV-B)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Support\Transliterator: fromLatin() (ICAO digraph/diacritic folding) and
  matches() to compare a visual-zone name against an MRZ name
- Visa optional_data now sliced by line length (16 on MRV-A, 8 on MRV-B)
- New FrenchIdMrzParser with the bespoke CNI layout (no expiry encoded)
- Detected by the 'IDFRA' prefix ahead of TD2; composite check digit uses
  the continuous cross-line 7-3-1 weighting specific to this format
- Add DocumentType::FRENCH_ID

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New FrenchIdMrzParser with the bespoke CNI layout (no expiry encoded)
- Detected by the 'IDFRA' prefix ahead of TD2; composite check digit uses
  the continuous cross-line 7-3-1 weighting specific to this format
- Add DocumentType::FRENCH_ID
- Auto-discovered MrzParserServiceProvider (binds MrzParser)
- ValidMrz validation rule (optional strict check-digit mode)
- Declare illuminate/support (dev + suggest) and laravel provider discovery

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-discovered MrzParserServiceProvider (binds MrzParser)
- ValidMrz validation rule (optional strict check-digit mode)
- Declare illuminate/support (dev + suggest) and laravel provider discovery
… rule

- MrzResult, tryParse/tryRead, strict mode
- Transliterator fromLatin/matches
- French national ID (verified GioviQ specimen) + tamper regression
- Visa optional-data lengths; ValidMrz rule
- Suite now 99 tests / ~99% coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rule

- MrzResult, tryParse/tryRead, strict mode
- Transliterator fromLatin/matches
- French national ID (verified GioviQ specimen) + tamper regression
- Visa optional-data lengths; ValidMrz rule
- Suite now 99 tests / ~99% coverage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rakibdevs
rakibdevs merged commit 11e2fe6 into main Jul 1, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant