Skip to content

Commit 11e2fe6

Browse files
authored
Merge pull request #17 from rakibdevs/hotfix/upgrade
v2.0.0 — PHP 8.2, check-digit validation, typed API, and new formats
2 parents 83efece + 8492f9a commit 11e2fe6

44 files changed

Lines changed: 1777 additions & 427 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/php-cs-fixer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88

99
steps:
1010
- name: Checkout code
11-
uses: actions/checkout@v3
11+
uses: actions/checkout@v4
1212
with:
1313
ref: ${{ github.head_ref }}
1414

.github/workflows/run-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ jobs:
99
fail-fast: true
1010
matrix:
1111
os: [ubuntu-latest, windows-latest]
12-
php: [8.0]
12+
php: [8.2, 8.3, 8.4, 8.5]
1313
stability: [prefer-lowest, prefer-stable]
1414

1515
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
1616

1717
steps:
1818
- name: Checkout code
19-
uses: actions/checkout@v3
19+
uses: actions/checkout@v4
2020

2121
- name: Setup PHP
2222
uses: shivammathur/setup-php@v2

.github/workflows/update-changelog.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010

1111
steps:
1212
- name: Checkout code
13-
uses: actions/checkout@v3
13+
uses: actions/checkout@v4
1414
with:
1515
ref: main
1616

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
All notable changes to `mrz-parser` will be documented in this file.
44

5+
## v2.0.0 - 2026-07-01
6+
7+
### Breaking changes
8+
9+
- Minimum PHP version is now **8.2** (was 8.0).
10+
- `parse()` now returns additional keys (`issuer_code`, `nationality_code`, `sex`, `optional_data`, `valid`, `validation`, `raw`). Existing keys are unchanged.
11+
- `Enums\DocumentType` is now a real backed enum instead of an abstract class with constants.
12+
13+
### Added
14+
15+
- **ICAO 9303 check-digit validation.** Each result exposes an overall `valid` flag plus a per-field `validation` map (document number, date of birth, date of expiry, personal number, and composite).
16+
- Richer output: raw ISO alpha-3 codes (`issuer_code`, `nationality_code`), raw `sex` character, `optional_data`, and the normalized `raw` MRZ string.
17+
- **Typed `MrzResult` object** via `MrzParser::read()` — typed properties, `dateOfBirthAsDate()`/`dateOfExpiryAsDate()`, `isExpired()`, `fullName()`, and `toArray()`.
18+
- **Non-throwing entry points** `MrzParser::tryParse()` and `MrzParser::tryRead()` returning `null` on invalid input.
19+
- **Strict check-digit mode** (`parse($text, strict: true)`) throwing `InvalidChecksumException` when a check digit fails.
20+
- **Name transliteration** (`Support\Transliterator`): `fromLatin()` and `matches()` for converting/comparing accented names against the MRZ.
21+
- **French national ID** (2×36 legacy format) support.
22+
- **Laravel integration**: auto-discovered service provider and a `ValidMrz` validation rule.
23+
- A full Pest test suite (parsers, validator, check digits, mappers, DTO, transliteration, Laravel rule) with ~99% coverage.
24+
25+
### Fixed
26+
27+
- CRLF / whitespace input is now normalized, so Windows- and OCR-sourced MRZ strings parse correctly instead of failing validation.
28+
- Strict date validation rejects impossible calendar dates (e.g. Feb 30) instead of silently rolling them over.
29+
- A date of birth that would resolve to the future is shifted back a century.
30+
- Removed a PHP 8.4 implicit-nullable deprecation and guarded against `substr(null, ...)` on missing rows.
31+
- Consistent trailing-filler trimming for given names across all document types.
32+
33+
### Changed
34+
35+
- Modernized the codebase: `declare(strict_types=1)`, typed properties, `match`-based adapter selection, and a cached country lookup table.
36+
- CI now tests PHP 8.2, 8.3, 8.4 and 8.5; upgraded to Pest 3.
37+
538
## v1.2.0 - 2022-06-30
639

740
**Full Changelog**: https://github.com/rakibdevs/mrz-parser/compare/v1.1.0...v1.2.0

README.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22
# MRZ (Machine Readable Zones) Parser for PHP
33

44
[![Latest Version on Packagist](https://img.shields.io/packagist/v/rakibdevs/mrz-parser.svg?style=flat-square)](https://packagist.org/packages/rakibdevs/mrz-parser)
5+
[![Tests](https://img.shields.io/github/actions/workflow/status/rakibdevs/mrz-parser/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/rakibdevs/mrz-parser/actions/workflows/run-tests.yml)
6+
[![PHP Version](https://img.shields.io/packagist/dependency-v/rakibdevs/mrz-parser/php?style=flat-square)](https://packagist.org/packages/rakibdevs/mrz-parser)
7+
[![Total Downloads](https://img.shields.io/packagist/dt/rakibdevs/mrz-parser.svg?style=flat-square)](https://packagist.org/packages/rakibdevs/mrz-parser)
8+
[![License](https://img.shields.io/packagist/l/rakibdevs/mrz-parser.svg?style=flat-square)](LICENSE.md)
59

610

711
A PHP package for MRZ (Machine Readable Zones) code parser for Passport, Visa & Travel Document (TD1 & TD2).
812

13+
It reads the machine-readable zone, extracts the document fields, and — new in v2 — verifies the ICAO 9303 check digits so you can tell whether the data is internally consistent.
14+
15+
## Requirements
16+
17+
- PHP 8.2 or higher
18+
919
## Installation
1020

1121
You can install the package via composer:
@@ -31,16 +41,97 @@ var_dump($data);
3141
"type": "Passport",
3242
"card_no": "L898902C3",
3343
"issuer": "Utopian",
44+
"issuer_code": "UTO",
3445
"date_of_expiry": "2012-04-15",
3546
"first_name": "ANNA MARIA",
3647
"last_name": "ERIKSSON",
3748
"date_of_birth": "1974-08-12",
3849
"gender": "Female",
50+
"sex": "F",
3951
"personal_number": "ZE184226B",
40-
"nationality": "Utopian"
52+
"optional_data": "ZE184226B",
53+
"nationality": "Utopian",
54+
"nationality_code": "UTO",
55+
"valid": true,
56+
"validation": {
57+
"card_no": true,
58+
"date_of_birth": true,
59+
"date_of_expiry": true,
60+
"personal_number": true,
61+
"composite": true
62+
},
63+
"raw": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10"
4164
}
4265
```
4366
67+
### Check-digit validation
68+
69+
Every result carries a `valid` flag (overall) and a `validation` map with the outcome of each ICAO 9303 check digit (`card_no`, `date_of_birth`, `date_of_expiry`, `personal_number` for passports, and the `composite`). Visas carry no composite check digit, so their `validation` map omits it. Use these to reject OCR mistakes or tampered documents.
70+
71+
### Input handling
72+
73+
Input is normalized before parsing: `\r\n` / `\r` line endings are converted to `\n`, surrounding whitespace is trimmed, and blank lines are dropped — so MRZ strings copied from Windows or OCR output parse the same as clean input. Dates are validated strictly (impossible dates such as Feb 30 return `null`), and a date of birth that would resolve to the future is shifted back a century.
74+
75+
## Advanced usage
76+
77+
### Typed result object
78+
79+
`read()` returns an `MrzResult` value object with typed properties and helpers, while `toArray()` gives you the same array as `parse()`:
80+
81+
```php
82+
$mrz = MrzParser::read($text);
83+
84+
$mrz->cardNo; // 'L898902C3'
85+
$mrz->fullName(); // 'ANNA MARIA ERIKSSON'
86+
$mrz->dateOfBirthAsDate(); // DateTimeImmutable
87+
$mrz->isExpired(); // bool (or null when the format has no expiry)
88+
$mrz->valid; // bool
89+
$mrz->toArray(); // the canonical parse() array
90+
```
91+
92+
### Non-throwing parsing
93+
94+
`parse()`/`read()` throw on unrecognised input. Use the `try*` variants to get `null` instead:
95+
96+
```php
97+
MrzParser::tryParse($text); // ?array
98+
MrzParser::tryRead($text); // ?MrzResult
99+
```
100+
101+
### Strict check-digit mode
102+
103+
Pass `strict: true` to reject documents whose check digits don't verify (throws `InvalidChecksumException`; the `try*` variants return `null`):
104+
105+
```php
106+
MrzParser::parse($text, strict: true);
107+
MrzParser::read($text, strict: true);
108+
```
109+
110+
### Name transliteration
111+
112+
The MRZ name field only holds `A–Z`. Convert a visual-zone (accented) name to its MRZ form, or compare the two:
113+
114+
```php
115+
use Rakibdevs\MrzParser\Support\Transliterator;
116+
117+
Transliterator::fromLatin('Müller'); // 'MUELLER'
118+
Transliterator::fromLatin('Jørgensen'); // 'JOERGENSEN'
119+
Transliterator::matches('MUELLER', 'Müller'); // true
120+
```
121+
122+
### Laravel
123+
124+
The package ships an auto-discovered service provider and a validation rule (requires `illuminate/support`):
125+
126+
```php
127+
use Rakibdevs\MrzParser\Laravel\Rules\ValidMrz;
128+
129+
$request->validate([
130+
'mrz' => [new ValidMrz()], // must be a parseable MRZ
131+
'mrz' => [new ValidMrz(strict: true)], // ...with valid check digits
132+
]);
133+
```
134+
44135
## Supported Document
45136
#### [Passport (TD3)](https://www.icao.int/publications/Documents/9303_p4_cons_en.pdf)
46137
```
@@ -70,6 +161,13 @@ I<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<
70161
D231458907UTO7408122F1204159<<<<<<<6
71162
```
72163

164+
#### French National ID (legacy 2×36 Carte Nationale d'Identité)
165+
This pre-2021 format predates ICAO TD1/TD2 and has its own layout (no expiry date is encoded).
166+
```
167+
IDFRADOUEL<<<<<<<<<<<<<<<<<<<<932013
168+
0506932020438CHRISTIANE<<NI2906209F3
169+
```
170+
73171

74172
## Changelog
75173

composer.json

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
}
1616
],
1717
"require": {
18-
"php": "^8.0"
18+
"php": "^8.2"
1919
},
2020
"require-dev": {
21-
"friendsofphp/php-cs-fixer": "^3.0",
22-
"pestphp/pest": "^1.20",
23-
"spatie/ray": "^1.28"
21+
"friendsofphp/php-cs-fixer": "^3.64",
22+
"illuminate/support": "^10.0|^11.0|^12.0",
23+
"pestphp/pest": "^3.0"
24+
},
25+
"suggest": {
26+
"illuminate/support": "Required to use the Laravel service provider and the ValidMrz validation rule."
2427
},
2528
"autoload": {
2629
"psr-4": {
@@ -38,7 +41,17 @@
3841
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
3942
},
4043
"config": {
41-
"sort-packages": true
44+
"sort-packages": true,
45+
"allow-plugins": {
46+
"pestphp/pest-plugin": true
47+
}
48+
},
49+
"extra": {
50+
"laravel": {
51+
"providers": [
52+
"Rakibdevs\\MrzParser\\Laravel\\MrzParserServiceProvider"
53+
]
54+
}
4255
},
4356
"minimum-stability": "dev",
4457
"prefer-stable": true

phpunit.xml.dist

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,27 @@
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
55
backupGlobals="false"
6-
backupStaticAttributes="false"
76
bootstrap="vendor/autoload.php"
87
colors="true"
9-
convertErrorsToExceptions="true"
10-
convertNoticesToExceptions="true"
11-
convertWarningsToExceptions="true"
128
processIsolation="false"
139
stopOnFailure="false"
1410
executionOrder="random"
1511
failOnWarning="true"
1612
failOnRisky="true"
1713
failOnEmptyTestSuite="true"
1814
beStrictAboutOutputDuringTests="true"
19-
verbose="true"
2015
>
2116
<testsuites>
2217
<testsuite name="Rakibdevs Test Suite">
2318
<directory>tests</directory>
2419
</testsuite>
2520
</testsuites>
26-
<coverage>
21+
<source>
2722
<include>
2823
<directory suffix=".php">./src</directory>
2924
</include>
25+
</source>
26+
<coverage>
3027
<report>
3128
<html outputDirectory="build/coverage"/>
3229
<text outputFile="build/coverage.txt"/>

src/Contracts/ParserInterface.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Rakibdevs\MrzParser\Contracts;
46

57
interface ParserInterface
68
{
7-
public function parse(string $text);
9+
public function parse(string $text): ?array;
810
}

src/Enums/DocumentType.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Rakibdevs\MrzParser\Enums;
46

5-
abstract class DocumentType
7+
enum DocumentType: string
68
{
7-
public const PASSPORT = "passport";
8-
public const VISA = "visa";
9-
public const TRAVEL_DOCUMENT_1 = "travel_document_1";
10-
public const TRAVEL_DOCUMENT_2 = "travel_document_2";
9+
case PASSPORT = 'passport';
10+
case VISA = 'visa';
11+
case TRAVEL_DOCUMENT_1 = 'travel_document_1';
12+
case TRAVEL_DOCUMENT_2 = 'travel_document_2';
13+
case FRENCH_ID = 'french_id';
1114
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rakibdevs\MrzParser\Exceptions;
6+
7+
use Exception;
8+
9+
class InvalidChecksumException extends Exception
10+
{
11+
}

0 commit comments

Comments
 (0)