├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Contracts └── ParserInterface.php ├── Enums └── DocumentType.php ├── Exceptions ├── InvalidFormatException.php └── NotSupportedException.php ├── MrzParser.php ├── Parser ├── PassportMrzParser.php ├── TravelDocument1MrzParser.php ├── TravelDocument2MrzParser.php └── VisaMrzParser.php ├── Traits ├── CountryMapper.php ├── DateFormatter.php └── GenderMapper.php └── ValidateDocument.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `mrz-parser` will be documented in this file. 4 | 5 | ## v1.2.0 - 2022-06-30 6 | 7 | **Full Changelog**: https://github.com/rakibdevs/mrz-parser/compare/v1.1.0...v1.2.0 8 | 9 | 1. Fixed Card No and First Name "<" Issue in Visa b292c6a 10 | 2. Added Semantic Versioning 11 | 12 | Special Thanks to @sohelamin for the Review. 13 | 14 | ## v1.1.0 - 2022-06-29 15 | 16 | ### What's Changed 17 | 18 | - Fixed #1 Replace the first name arrow with space. 19 | - Fixed showing wrong passport number (0f66162:) 20 | - Validate given date format in MRZ (ede0247) 21 | 22 | ### New Contributors 23 | 24 | - @alaminfirdows made their first contribution in https://github.com/rakibdevs/mrz-parser/pull/2 25 | 26 | **Full Changelog**: https://github.com/rakibdevs/mrz-parser/compare/v1.0...v1.1 27 | 28 | ## v1.0.0 - 2022-06-26 29 | 30 | Extract information from Machine Readable Zone data for Passport, Visa, and Other Travel Documents. 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) rakibdevs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # MRZ (Machine Readable Zones) Parser for PHP 3 | 4 | [![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 | 6 | 7 | A PHP package for MRZ (Machine Readable Zones) code parser for Passport, Visa & Travel Document (TD1 & TD2). 8 | 9 | ## Installation 10 | 11 | You can install the package via composer: 12 | 13 | ```bash 14 | composer require rakibdevs/mrz-parser 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```php 20 | use Rakibdevs\MrzParser\MrzParser; 21 | ..... 22 | ..... 23 | $data = MrzParser::parse('Ptext = $text; 23 | } 24 | 25 | protected function setAdapter() 26 | { 27 | switch ($this->documentType) { 28 | case DocumentType::PASSPORT: 29 | $this->adapter = new PassportMrzParser(); 30 | 31 | break; 32 | case DocumentType::VISA: 33 | $this->adapter = new VisaMrzParser(); 34 | 35 | break; 36 | case DocumentType::TRAVEL_DOCUMENT_1: 37 | $this->adapter = new TravelDocument1MrzParser(); 38 | 39 | break; 40 | case DocumentType::TRAVEL_DOCUMENT_2: 41 | $this->adapter = new TravelDocument2MrzParser(); 42 | 43 | break; 44 | } 45 | 46 | return $this; 47 | } 48 | 49 | protected function validate() 50 | { 51 | $this->documentType = (new ValidateDocument($this->text))->validate(); 52 | 53 | return $this; 54 | } 55 | 56 | protected function get(): ?array 57 | { 58 | if (empty($this->adapter)) { 59 | throw new NotSupportedException("This format is not supported yet!"); 60 | } 61 | 62 | return $this->adapter->parse($this->text); 63 | } 64 | 65 | public static function parse(string $text): array 66 | { 67 | return (new static($text)) 68 | ->validate() 69 | ->setAdapter() 70 | ->get(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Parser/PassportMrzParser.php: -------------------------------------------------------------------------------- 1 | text = $text; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Set Name String 42 | * 43 | * @return self 44 | */ 45 | protected function setNameString(): self 46 | { 47 | $this->nameString = explode('<<', substr($this->firstLine, 5)); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Extract information 54 | * 55 | * @return self 56 | */ 57 | protected function extract(): self 58 | { 59 | $text = explode("\n", $this->text); 60 | $this->firstLine = $text[0] ?? null; 61 | $this->secondLine = $text[1] ?? null; 62 | $this->setNameString(); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Second row first 9 character alpha+num+< Passport number 69 | * 70 | * @return null|string 71 | */ 72 | protected function getCardNo(): ?string 73 | { 74 | $cardNo = substr($this->secondLine, 0, 9); 75 | $cardNo = chop($cardNo, "<"); // remove extra '<' from card no 76 | 77 | return $cardNo; 78 | } 79 | 80 | /** 81 | * Get Passport Issuer 82 | * 83 | * @return null|string 84 | */ 85 | protected function getIssuer(): ?string 86 | { 87 | $issuer = chop(substr($this->firstLine, 2, 3), "<"); 88 | 89 | return $this->mapCountry($issuer); 90 | } 91 | 92 | /** 93 | * Get Date of Expiry 94 | * Second row 22–27 character: (YYMMDD) 95 | * 96 | * @return null|string 97 | */ 98 | protected function getDateOfExpiry(): ?string 99 | { 100 | $date = substr($this->secondLine, 21, 6); 101 | 102 | return $date ? $this->formatDate($date) : null; 103 | } 104 | 105 | /** 106 | * Get Date of Birth 107 | * Second row 14–19 character: (YYMMDD) 108 | * 109 | * @return null|string 110 | */ 111 | protected function getDateOfBirth(): ?string 112 | { 113 | $date = substr($this->secondLine, 13, 6); 114 | 115 | return $date ? $this->formatDate($date) : null; 116 | } 117 | 118 | /** 119 | * Get First Name from Name String 120 | * For Ex, MARTINA<<<<<<<<<<<<<<<<<<<<<<<<<< 121 | * 122 | * @return null|string 123 | */ 124 | protected function getFirstName(): ?string 125 | { 126 | return isset($this->nameString[1]) ? str_replace('<', ' ', chop($this->nameString[1], "<")) : null; 127 | } 128 | 129 | /** 130 | * Get Last Name from Name String 131 | * 132 | * @return null|string 133 | */ 134 | protected function getLastName(): ?string 135 | { 136 | return $this->nameString[0] ?? null; 137 | } 138 | 139 | /** 140 | * Get Gender from Position 21, M/F/< 141 | * 142 | * @return null|string 143 | * 144 | */ 145 | protected function getGender(): ?string 146 | { 147 | return $this->mapGender(substr($this->secondLine, 20, 1)); 148 | } 149 | 150 | /** 151 | * Get Personal Number 152 | * 29–42 alpha+num+< (may be used by the issuing country as it desires) 153 | * 154 | * @return null|string 155 | */ 156 | protected function getPersonalNumber(): ?string 157 | { 158 | return chop(substr($this->secondLine, 28, 14), "<"); 159 | } 160 | 161 | /** 162 | * Get Nationality 163 | * 164 | * @return null|string 165 | */ 166 | protected function getNationality(): ?string 167 | { 168 | $code = chop(substr($this->secondLine, 10, 3), "<"); 169 | 170 | return $this->mapCountry($code); 171 | } 172 | 173 | /** 174 | * Get Output from MRZ 175 | * 176 | * @return array 177 | */ 178 | protected function get(): array 179 | { 180 | return [ 181 | 'type' => 'Passport', 182 | 'card_no' => $this->getCardNo(), 183 | 'issuer' => $this->getIssuer(), 184 | 'date_of_expiry' => $this->getDateOfExpiry(), 185 | 'first_name' => $this->getFirstName(), 186 | 'last_name' => $this->getLastName(), 187 | 'date_of_birth' => $this->getDateOfBirth(), 188 | 'gender' => $this->getGender(), 189 | 'personal_number' => $this->getPersonalNumber(), 190 | 'nationality' => $this->getNationality(), 191 | ]; 192 | } 193 | 194 | /** 195 | * Parse MRZ to Json Data 196 | * 197 | * @param string $text 198 | * @return null|array 199 | */ 200 | public function parse(string $text): ?array 201 | { 202 | return $this 203 | ->setText($text) 204 | ->extract() 205 | ->get(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Parser/TravelDocument1MrzParser.php: -------------------------------------------------------------------------------- 1 | text = $text; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set Name String 43 | */ 44 | protected function setNameString(): self 45 | { 46 | $this->nameString = explode('<<', $this->thirdLine); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Extract information 53 | */ 54 | protected function extract(): self 55 | { 56 | $text = explode("\n", $this->text); 57 | $this->firstLine = $text[0] ?? null; 58 | $this->secondLine = $text[1] ?? null; 59 | $this->thirdLine = $text[2] ?? null; 60 | $this->setNameString(); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Get Type beased on first two string 67 | * 68 | * Type, This is at the discretion of the issuing state or authority, 69 | * but 1–2 should be AC for Crew Member Certificates and V is not allowed as 2nd character. 70 | * ID or I< are typically used for nationally issued ID cards and IP for passport cards. 71 | */ 72 | protected function getType() 73 | { 74 | $firstTwoCharacter = substr($this->firstLine, 0, 2); 75 | 76 | return match ($firstTwoCharacter) { 77 | 'AC' => 'Crew Member Certificates', 78 | 'I<' => 'National ID', 79 | 'IP' => 'Passport', 80 | default => "Travel Document (TD1)" 81 | }; 82 | } 83 | 84 | /** 85 | * Get Document Number 86 | * 6–14 alpha+num+< Document number 87 | */ 88 | protected function getCardNo(): ?string 89 | { 90 | $cardNo = substr($this->firstLine, 5, 9); 91 | $cardNo = chop($cardNo, "<"); // remove extra '<' from card no 92 | 93 | return $cardNo; 94 | } 95 | 96 | /** 97 | * Get Document Issuer 98 | */ 99 | protected function getIssuer(): ?string 100 | { 101 | $issuer = chop(substr($this->firstLine, 2, 3), "<"); 102 | 103 | return $this->mapCountry($issuer); 104 | } 105 | 106 | /** 107 | * Get Date of Expiry 108 | * Second row 9-14 character: (YYMMDD) 109 | */ 110 | protected function getDateOfExpiry(): ?string 111 | { 112 | $date = substr($this->secondLine, 8, 6); 113 | 114 | return $date ? $this->formatDate($date) : null; 115 | } 116 | 117 | /** 118 | * Get Date of Birth 119 | * Second row 1-6 character: (YYMMDD) 120 | */ 121 | protected function getDateOfBirth(): ?string 122 | { 123 | $date = substr($this->secondLine, 0, 6); 124 | 125 | return $date ? $this->formatDate($date) : null; 126 | } 127 | 128 | /** 129 | * Get First Name from Name String 130 | * 131 | * <nameString[1]) ? str_replace("<", " ", $this->nameString[1]) : null; 136 | } 137 | 138 | /** 139 | * Get Last Name from Name String 140 | */ 141 | protected function getLastName(): ?string 142 | { 143 | return $this->nameString[0] ?? null; 144 | } 145 | 146 | /** 147 | * Get Gender 148 | * Position 8, M/F/< 149 | * 150 | */ 151 | protected function getGender(): ?string 152 | { 153 | return $this->mapGender(substr($this->secondLine, 7, 1)); 154 | } 155 | 156 | /** 157 | * Get Nationality 158 | */ 159 | protected function getNationality(): ?string 160 | { 161 | $code = chop(substr($this->secondLine, 15, 3), "<"); 162 | 163 | return $this->mapCountry($code); 164 | } 165 | 166 | /** 167 | * Get Output from MRZ 168 | * 169 | * @return array 170 | */ 171 | protected function get(): array 172 | { 173 | return [ 174 | 'type' => $this->getType(), 175 | 'card_no' => $this->getCardNo(), 176 | 'issuer' => $this->getIssuer(), 177 | 'date_of_expiry' => $this->getDateOfExpiry(), 178 | 'first_name' => $this->getFirstName(), 179 | 'last_name' => $this->getLastName(), 180 | 'date_of_birth' => $this->getDateOfBirth(), 181 | 'gender' => $this->getGender(), 182 | 'nationality' => $this->getNationality(), 183 | ]; 184 | } 185 | 186 | /** 187 | * Parse MRZ to Json Data 188 | * 189 | * @param string $text 190 | * @return null|array 191 | */ 192 | public function parse(string $text): ?array 193 | { 194 | return $this 195 | ->setText($text) 196 | ->extract() 197 | ->get(); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Parser/TravelDocument2MrzParser.php: -------------------------------------------------------------------------------- 1 | text = $text; 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Set Name String 40 | */ 41 | protected function setNameString(): self 42 | { 43 | $this->nameString = explode('<<', substr($this->firstLine, 5)); 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Extract information 50 | */ 51 | protected function extract(): self 52 | { 53 | $text = explode("\n", $this->text); 54 | $this->firstLine = $text[0] ?? null; 55 | $this->secondLine = $text[1] ?? null; 56 | $this->setNameString(); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Get Type beased on first two string 63 | * 64 | * Type, This is at the discretion of the issuing state or authority, 65 | * but 1–2 should be AC for Crew Member Certificates and V is not allowed as 2nd character. 66 | * ID or I< are typically used for nationally issued ID cards and IP for passport cards. 67 | */ 68 | protected function getType() 69 | { 70 | $firstTwoCharacter = substr($this->firstLine, 0, 2); 71 | 72 | return match ($firstTwoCharacter) { 73 | 'AC' => 'Crew Member Certificates', 74 | 'I<' => 'National ID', 75 | 'IP' => 'Passport', 76 | default => "Travel Document (TD2)" 77 | }; 78 | } 79 | 80 | /** 81 | * Get Document Number 82 | * 1-9 alpha+num+< Document number 83 | */ 84 | protected function getCardNo(): ?string 85 | { 86 | $cardNo = substr($this->secondLine, 0, 9); 87 | $cardNo = chop($cardNo, "<"); // remove extra '<' from card no 88 | 89 | return $cardNo; 90 | } 91 | 92 | /** 93 | * Get Document Issuer 94 | */ 95 | protected function getIssuer(): ?string 96 | { 97 | $issuer = chop(substr($this->firstLine, 2, 3), "<"); 98 | 99 | return $this->mapCountry($issuer); 100 | } 101 | 102 | /** 103 | * Get Date of Expiry 104 | * Second row 22-27 character: (YYMMDD) 105 | */ 106 | protected function getDateOfExpiry(): ?string 107 | { 108 | $date = substr($this->secondLine, 21, 6); 109 | 110 | return $date ? $this->formatDate($date) : null; 111 | } 112 | 113 | /** 114 | * Get Date of Birth 115 | * Second row 14-19 character: (YYMMDD) 116 | */ 117 | protected function getDateOfBirth(): ?string 118 | { 119 | $date = substr($this->secondLine, 13, 6); 120 | 121 | return $date ? $this->formatDate($date) : null; 122 | } 123 | 124 | /** 125 | * Get First Name from Name String 126 | * 127 | * <nameString[1]) ? str_replace("<", " ", $this->nameString[1]) : null; 132 | } 133 | 134 | /** 135 | * Get Last Name from Name String 136 | */ 137 | protected function getLastName(): ?string 138 | { 139 | return $this->nameString[0] ?? null; 140 | } 141 | 142 | /** 143 | * Get Gender 144 | * Position 21, M/F/< 145 | * 146 | */ 147 | protected function getGender(): ?string 148 | { 149 | return $this->mapGender(substr($this->secondLine, 20, 1)); 150 | } 151 | 152 | /** 153 | * Get Nationality 154 | */ 155 | protected function getNationality(): ?string 156 | { 157 | $code = chop(substr($this->secondLine, 10, 3), "<"); 158 | 159 | return $this->mapCountry($code); 160 | } 161 | 162 | /** 163 | * Get Output from MRZ 164 | * 165 | * @return array 166 | */ 167 | protected function get(): array 168 | { 169 | return [ 170 | 'type' => $this->getType(), 171 | 'card_no' => $this->getCardNo(), 172 | 'issuer' => $this->getIssuer(), 173 | 'date_of_expiry' => $this->getDateOfExpiry(), 174 | 'first_name' => $this->getFirstName(), 175 | 'last_name' => $this->getLastName(), 176 | 'date_of_birth' => $this->getDateOfBirth(), 177 | 'gender' => $this->getGender(), 178 | 'nationality' => $this->getNationality(), 179 | ]; 180 | } 181 | 182 | /** 183 | * Parse MRZ to Json Data 184 | * 185 | * @param string $text 186 | * @return null|array 187 | */ 188 | public function parse(string $text): ?array 189 | { 190 | return $this 191 | ->setText($text) 192 | ->extract() 193 | ->get(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Parser/VisaMrzParser.php: -------------------------------------------------------------------------------- 1 | text = $text; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * Set Name String 39 | */ 40 | protected function setNameString(): self 41 | { 42 | $this->nameString = explode('<<', substr($this->firstLine, 5)); 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Extract information 49 | */ 50 | protected function extract(): self 51 | { 52 | $text = explode("\n", $this->text); 53 | $this->firstLine = $text[0] ?? null; 54 | $this->secondLine = $text[1] ?? null; 55 | $this->setNameString(); 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Second row first 9 character alpha+num+< Visa number 62 | */ 63 | protected function getCardNo(): ?string 64 | { 65 | $cardNo = substr($this->secondLine, 0, 9); 66 | $cardNo = chop($cardNo, "<"); // remove extra '<' from card no 67 | 68 | return $cardNo; 69 | } 70 | 71 | /** 72 | * Get Visa Issuer 73 | */ 74 | protected function getIssuer(): ?string 75 | { 76 | $issuer = chop(substr($this->firstLine, 2, 3), "<"); 77 | 78 | return $this->mapCountry($issuer); 79 | } 80 | 81 | /** 82 | * Get Date of Expiry 83 | * Second row 22–27 character: (YYMMDD) 84 | */ 85 | protected function getDateOfExpiry(): ?string 86 | { 87 | $date = substr($this->secondLine, 21, 6); 88 | 89 | return $date ? $this->formatDate($date) : null; 90 | } 91 | 92 | /** 93 | * Get Date of Birth 94 | * Second row 14–19 character: (YYMMDD) 95 | */ 96 | protected function getDateOfBirth(): ?string 97 | { 98 | $date = substr($this->secondLine, 13, 6); 99 | 100 | return $date ? $this->formatDate($date) : null; 101 | } 102 | 103 | /** 104 | * Get First Name from Name String 105 | * 106 | * MARTINA<<<<<<<<<<<<<<<<<<<<<<<<<< 107 | */ 108 | protected function getFirstName(): ?string 109 | { 110 | return isset($this->nameString[1]) ? str_replace('<', ' ', chop($this->nameString[1], "<")) : null; 111 | } 112 | 113 | /** 114 | * Get Last Name from Name String 115 | * 116 | */ 117 | protected function getLastName(): ?string 118 | { 119 | return $this->nameString[0] ?? null; 120 | } 121 | 122 | /** 123 | * Get Gender 124 | * Position 21, M/F/< 125 | * 126 | */ 127 | protected function getGender(): ?string 128 | { 129 | return $this->mapGender(substr($this->secondLine, 20, 1)); 130 | } 131 | 132 | /** 133 | * Get Nationality 134 | */ 135 | protected function getNationality(): ?string 136 | { 137 | $code = chop(substr($this->secondLine, 10, 3), "<"); 138 | 139 | return $this->mapCountry($code); 140 | } 141 | 142 | /** 143 | * Get Output from MRZ 144 | * 145 | * @return array 146 | */ 147 | protected function get(): array 148 | { 149 | return [ 150 | 'type' => 'Visa', 151 | 'card_no' => $this->getCardNo(), 152 | 'issuer' => $this->getIssuer(), 153 | 'date_of_expiry' => $this->getDateOfExpiry(), 154 | 'first_name' => $this->getFirstName(), 155 | 'last_name' => $this->getLastName(), 156 | 'date_of_birth' => $this->getDateOfBirth(), 157 | 'gender' => $this->getGender(), 158 | 'nationality' => $this->getNationality(), 159 | ]; 160 | } 161 | 162 | /** 163 | * Parse MRZ to Json Data 164 | * 165 | * @param string $text 166 | * @return null|array 167 | */ 168 | public function parse(string $text): ?array 169 | { 170 | return $this 171 | ->setText($text) 172 | ->extract() 173 | ->get(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Traits/CountryMapper.php: -------------------------------------------------------------------------------- 1 | "Afghanistan", 18 | "ALB" => "Albania", 19 | "DZA" => "Algeria", 20 | "ASM" => "American Samoa", 21 | "AND" => "Andorra", 22 | "AGO" => "Angola", 23 | "AIA" => "Anguilla", 24 | "ATA" => "Antarctica", 25 | "ATG" => "Antigua and Barbuda", 26 | "ARG" => "Argentina", 27 | "ARM" => "Armenia", 28 | "ABW" => "Aruba", 29 | "AUS" => "Australia", 30 | "AUT" => "Austria", 31 | "AZE" => "Azerbaijan", 32 | "BHS" => "Bahamas (the)", 33 | "BHR" => "Bahrain", 34 | "BGD" => "Bangladesh", 35 | "BRB" => "Barbados", 36 | "BLR" => "Belarus", 37 | "BEL" => "Belgium", 38 | "BLZ" => "Belize", 39 | "BEN" => "Benin", 40 | "BMU" => "Bermuda", 41 | "BTN" => "Bhutan", 42 | "BOL" => "Bolivia (Plurinational State of)", 43 | "BES" => "Bonaire, Sint Eustatius and Saba", 44 | "BIH" => "Bosnia and Herzegovina", 45 | "BWA" => "Botswana", 46 | "BVT" => "Bouvet Island", 47 | "BRA" => "Brazil", 48 | "IOT" => "British Indian Ocean Territory (the)", 49 | "BRN" => "Brunei Darussalam", 50 | "BGR" => "Bulgaria", 51 | "BFA" => "Burkina Faso", 52 | "BDI" => "Burundi", 53 | "CPV" => "Cabo Verde", 54 | "KHM" => "Cambodia", 55 | "CMR" => "Cameroon", 56 | "CAN" => "Canada", 57 | "CYM" => "Cayman Islands (the)", 58 | "CAF" => "Central African Republic (the)", 59 | "TCD" => "Chad", 60 | "CHL" => "Chile", 61 | "CHN" => "China", 62 | "CXR" => "Christmas Island", 63 | "CCK" => "Cocos (Keeling) Islands (the)", 64 | "COL" => "Colombia", 65 | "COM" => "Comoros (the)", 66 | "COD" => "Congo (the Democratic Republic of the)", 67 | "COG" => "Congo (the)", 68 | "COK" => "Cook Islands (the)", 69 | "CRI" => "Costa Rica", 70 | "HRV" => "Croatia", 71 | "CUB" => "Cuba", 72 | "CUW" => "Curaçao", 73 | "CYP" => "Cyprus", 74 | "CZE" => "Czechia", 75 | "CIV" => "Côte d'Ivoire", 76 | "DNK" => "Denmark", 77 | "DJI" => "Djibouti", 78 | "DMA" => "Dominica", 79 | "DOM" => "Dominican Republic (the)", 80 | "ECU" => "Ecuador", 81 | "EGY" => "Egypt", 82 | "SLV" => "El Salvador", 83 | "GNQ" => "Equatorial Guinea", 84 | "ERI" => "Eritrea", 85 | "EST" => "Estonia", 86 | "SWZ" => "Eswatini", 87 | "ETH" => "Ethiopia", 88 | "FLK" => "Falkland Islands (the) [Malvinas]", 89 | "FRO" => "Faroe Islands (the)", 90 | "FJI" => "Fiji", 91 | "FIN" => "Finland", 92 | "FRA" => "France", 93 | "GUF" => "French Guiana", 94 | "PYF" => "French Polynesia", 95 | "ATF" => "French Southern Territories (the)", 96 | "GAB" => "Gabon", 97 | "GMB" => "Gambia (the)", 98 | "GEO" => "Georgia", 99 | "DEU" => "Germany", 100 | "GHA" => "Ghana", 101 | "GIB" => "Gibraltar", 102 | "GRC" => "Greece", 103 | "GRL" => "Greenland", 104 | "GRD" => "Grenada", 105 | "GLP" => "Guadeloupe", 106 | "GUM" => "Guam", 107 | "GTM" => "Guatemala", 108 | "GGY" => "Guernsey", 109 | "GIN" => "Guinea", 110 | "GNB" => "Guinea-Bissau", 111 | "GUY" => "Guyana", 112 | "HTI" => "Haiti", 113 | "HMD" => "Heard Island and McDonald Islands", 114 | "VAT" => "Holy See (the)", 115 | "HND" => "Honduras", 116 | "HKG" => "Hong Kong", 117 | "HUN" => "Hungary", 118 | "ISL" => "Iceland", 119 | "IND" => "India", 120 | "IDN" => "Indonesia", 121 | "IRN" => "Iran (Islamic Republic of)", 122 | "IRQ" => "Iraq", 123 | "IRL" => "Ireland", 124 | "IMN" => "Isle of Man", 125 | "ISR" => "Israel", 126 | "ITA" => "Italy", 127 | "JAM" => "Jamaica", 128 | "JPN" => "Japan", 129 | "JEY" => "Jersey", 130 | "JOR" => "Jordan", 131 | "KAZ" => "Kazakhstan", 132 | "KEN" => "Kenya", 133 | "KIR" => "Kiribati", 134 | "PRK" => "Korea (the Democratic People's Republic of)", 135 | "KOR" => "Korea (the Republic of)", 136 | "KWT" => "Kuwait", 137 | "KGZ" => "Kyrgyzstan", 138 | "LAO" => "Lao People's Democratic Republic (the)", 139 | "LVA" => "Latvia", 140 | "LBN" => "Lebanon", 141 | "LSO" => "Lesotho", 142 | "LBR" => "Liberia", 143 | "LBY" => "Libya", 144 | "LIE" => "Liechtenstein", 145 | "LTU" => "Lithuania", 146 | "LUX" => "Luxembourg", 147 | "MAC" => "Macao", 148 | "MDG" => "Madagascar", 149 | "MWI" => "Malawi", 150 | "MYS" => "Malaysia", 151 | "MDV" => "Maldives", 152 | "MLI" => "Mali", 153 | "MLT" => "Malta", 154 | "MHL" => "Marshall Islands (the)", 155 | "MTQ" => "Martinique", 156 | "MRT" => "Mauritania", 157 | "MUS" => "Mauritius", 158 | "MYT" => "Mayotte", 159 | "MEX" => "Mexico", 160 | "FSM" => "Micronesia (Federated States of)", 161 | "MDA" => "Moldova (the Republic of)", 162 | "MCO" => "Monaco", 163 | "MNG" => "Mongolia", 164 | "MNE" => "Montenegro", 165 | "MSR" => "Montserrat", 166 | "MAR" => "Morocco", 167 | "MOZ" => "Mozambique", 168 | "MMR" => "Myanmar", 169 | "NAM" => "Namibia", 170 | "NRU" => "Nauru", 171 | "NPL" => "Nepal", 172 | "NLD" => "Netherlands (the)", 173 | "NCL" => "New Caledonia", 174 | "NZL" => "New Zealand", 175 | "NIC" => "Nicaragua", 176 | "NER" => "Niger (the)", 177 | "NGA" => "Nigeria", 178 | "NIU" => "Niue", 179 | "NFK" => "Norfolk Island", 180 | "MKD" => "North Macedonia", 181 | "MNP" => "Northern Mariana Islands (the)", 182 | "NOR" => "Norway", 183 | "OMN" => "Oman", 184 | "PAK" => "Pakistan", 185 | "PLW" => "Palau", 186 | "PSE" => "Palestine, State of", 187 | "PAN" => "Panama", 188 | "PNG" => "Papua New Guinea", 189 | "PRY" => "Paraguay", 190 | "PER" => "Peru", 191 | "PHL" => "Philippines (the)", 192 | "PCN" => "Pitcairn", 193 | "POL" => "Poland", 194 | "PRT" => "Portugal", 195 | "PRI" => "Puerto Rico", 196 | "QAT" => "Qatar", 197 | "ROU" => "Romania", 198 | "RUS" => "Russian Federation (the)", 199 | "RWA" => "Rwanda", 200 | "REU" => "Réunion", 201 | "BLM" => "Saint Barthélemy", 202 | "SHN" => "Saint Helena, Ascension and Tristan da Cunha", 203 | "KNA" => "Saint Kitts and Nevis", 204 | "LCA" => "Saint Lucia", 205 | "MAF" => "Saint Martin (French part)", 206 | "SPM" => "Saint Pierre and Miquelon", 207 | "VCT" => "Saint Vincent and the Grenadines", 208 | "WSM" => "Samoa", 209 | "SMR" => "San Marino", 210 | "STP" => "Sao Tome and Principe", 211 | "SAU" => "Saudi Arabia", 212 | "SEN" => "Senegal", 213 | "SRB" => "Serbia", 214 | "SYC" => "Seychelles", 215 | "SLE" => "Sierra Leone", 216 | "SGP" => "Singapore", 217 | "SXM" => "Sint Maarten (Dutch part)", 218 | "SVK" => "Slovakia", 219 | "SVN" => "Slovenia", 220 | "SLB" => "Solomon Islands", 221 | "SOM" => "Somalia", 222 | "ZAF" => "South Africa", 223 | "SGS" => "South Georgia and the South Sandwich Islands", 224 | "SSD" => "South Sudan", 225 | "ESP" => "Spain", 226 | "LKA" => "Sri Lanka", 227 | "SDN" => "Sudan (the)", 228 | "SUR" => "Suriname", 229 | "SJM" => "Svalbard and Jan Mayen", 230 | "SWE" => "Sweden", 231 | "CHE" => "Switzerland", 232 | "SYR" => "Syrian Arab Republic (the)", 233 | "TWN" => "Taiwan (Province of China)", 234 | "TJK" => "Tajikistan", 235 | "TZA" => "Tanzania, the United Republic of", 236 | "THA" => "Thailand", 237 | "TLS" => "Timor-Leste", 238 | "TGO" => "Togo", 239 | "TKL" => "Tokelau", 240 | "TON" => "Tonga", 241 | "TTO" => "Trinidad and Tobago", 242 | "TUN" => "Tunisia", 243 | "TUR" => "Turkey", 244 | "TKM" => "Turkmenistan", 245 | "TCA" => "Turks and Caicos Islands (the)", 246 | "TUV" => "Tuvalu", 247 | "UGA" => "Uganda", 248 | "UKR" => "Ukraine", 249 | "ARE" => "United Arab Emirates (the)", 250 | "GBR" => "United Kingdom of Great Britain and Northern Ireland (the)", 251 | "UMI" => "United States Minor Outlying Islands (the)", 252 | "USA" => "United States of America (the)", 253 | "URY" => "Uruguay", 254 | "UZB" => "Uzbekistan", 255 | "VUT" => "Vanuatu", 256 | "VEN" => "Venezuela (Bolivarian Republic of)", 257 | "VNM" => "Viet Nam", 258 | "VGB" => "Virgin Islands (British)", 259 | "VIR" => "Virgin Islands (U.S.)", 260 | "WLF" => "Wallis and Futuna", 261 | "ESH" => "Western Sahara*", 262 | "YEM" => "Yemen", 263 | "ZMB" => "Zambia", 264 | "ZWE" => "Zimbabwe", 265 | "ALA" => "Åland Islands", 266 | ]; 267 | 268 | protected $nonIso3166 = [ 269 | "XBA" => "African Development Bank", 270 | "XIM" => "African Export–Import Bank", 271 | "XCC" => "Caribbean Community", 272 | "XCO" => "Common Market for Eastern and Southern Africa", 273 | "XEC" => "Economic Community of West African States", 274 | "EUE" => "European Union", 275 | "D" => "Germany", 276 | "XPO" => "International Criminal Police Organization (Interpol)", 277 | "IMO" => "International Maritime Organisation", 278 | "RKS" => "Kosovo", 279 | "XOM" => "Sovereign Military Order of Malta", 280 | "WSA" => "World Service Authority World Passport", 281 | ]; 282 | 283 | /** 284 | * UNK = Travel document issued by the United Nations Interim Administration Mission in Kosovo (UNMIK) for Resident of Kosovo 285 | * XAA = Stateless person, as per the 1954 Convention Relating to the Status of Stateless Persons 286 | * XXB = Refugee, as per the 1951 Convention Relating to the Status of Refugees 287 | */ 288 | protected $unitedNations = [ 289 | "UNK" => "United Nations Interim Administration Mission in Kosovo (UNMIK)", 290 | "UNO" => "United Nations Organization Official", 291 | "UNA" => "United Nations Organization Specialized Agency Official", 292 | "XAA" => "Stateless (per Article 1 of 1954 convention)", 293 | "XXB" => "Refugee (per Article 1 of 1951 convention, amended by 1967 protocol)", 294 | "XXC" => "Refugee (non-convention)", 295 | "XXX" => "Unspecified Nationality / Unknown", 296 | "UTO" => "Utopian", 297 | ]; 298 | 299 | protected $britishNations = [ 300 | "GBR" => "United Kingdom of Great Britain and Northern Ireland Citizen", // British National (Proper) 301 | "GBD" => "United Kingdom of Great Britain and Northern Ireland Dependent Territories Citizen", // British Overseas Territories Citizen (BOTC) 302 | "GBN" => "United Kingdom of Great Britain and Northern Ireland National (Overseas)", // British National (Overseas) 303 | "GBO" => "United Kingdom of Great Britain and Northern Ireland Oversees Citizen", // British Overseas Citizen 304 | "GBP" => "United Kingdom of Great Britain and Northern Ireland Protected Person", // British Protected Person 305 | "GBS" => "United Kingdom of Great Britain and Northern Ireland Subject", // British Subject 306 | ]; 307 | 308 | /** 309 | * Map country name based on code 310 | * 311 | * @param string|null $code 312 | * @return string|null 313 | */ 314 | public function mapCountry(string $code = null): ?string 315 | { 316 | $allCountry = array_merge( 317 | $this->countries, 318 | $this->nonIso3166, 319 | $this->unitedNations, 320 | $this->britishNations 321 | ); 322 | 323 | return (array_key_exists($code, $allCountry)) ? $allCountry[$code] : null; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Traits/DateFormatter.php: -------------------------------------------------------------------------------- 1 | validateDateFormat($date)) { 20 | $dateTime = DateTime::createFromFormat('ymd', $date); 21 | 22 | return $dateTime->format($format); 23 | } 24 | 25 | return null; 26 | } 27 | 28 | /** 29 | * Validate Date Format YYMMDD 30 | * MM range must be 01-12 31 | * DD range must be 01-31 32 | * 33 | * @param string $date 34 | * @return bool 35 | */ 36 | public function validateDateFormat(string $date): bool 37 | { 38 | $month = (int) substr($date, 2, 2); 39 | $date = (int) substr($date, 4, 2); 40 | 41 | return $month >= 1 && $month <= 12 && $date >= 1 && $date <= 31; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Traits/GenderMapper.php: -------------------------------------------------------------------------------- 1 | "Male", 17 | "F" => "Female", 18 | default => null 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ValidateDocument.php: -------------------------------------------------------------------------------- 1 | text = $text; 23 | } 24 | 25 | protected function setProperties() 26 | { 27 | $rows = explode("\n", $this->text); 28 | $this->rows = count($rows); 29 | $this->characterCountOfRow = isset($rows[0]) ? strlen($rows[0]) : 0; 30 | $this->firstCharacter = substr($this->text, 0, 1); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Passport (TD3) 37 | * 2 rows, 44 character each row, start with 'P' 38 | */ 39 | protected function isPassport(): bool 40 | { 41 | if ($this->rows == 2 && $this->firstCharacter == 'P' && $this->characterCountOfRow == 44) { 42 | $this->documentType = DocumentType::PASSPORT; 43 | 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * Visa: 52 | * 2 rows, 36/44 character each row, start with 'V' 53 | */ 54 | protected function isVisa(): bool 55 | { 56 | if ($this->rows == 2 && $this->firstCharacter == 'V' && in_array($this->characterCountOfRow, [44, 36])) { 57 | $this->documentType = DocumentType::VISA; 58 | 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | /** 66 | * Travel Document (TD1) 67 | * 3 rows, 30 character each row, start with 'I/A/C' 68 | */ 69 | protected function isTravelDocument1(): bool 70 | { 71 | if ($this->rows == 3 && in_array($this->firstCharacter, ["I", "A", "C"]) && $this->characterCountOfRow == 30) { 72 | $this->documentType = DocumentType::TRAVEL_DOCUMENT_1; 73 | 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /** 81 | * Travel Document (TD2) 82 | * 2 rows, 36 character each row, start with 'I/P/A/C' 83 | */ 84 | protected function isTravelDocument2(): bool 85 | { 86 | if ($this->rows == 2 && in_array($this->firstCharacter, ["I", "P", "A", "C"]) && $this->characterCountOfRow == 36) { 87 | $this->documentType = DocumentType::TRAVEL_DOCUMENT_2; 88 | 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /** 96 | * Validate Document Based on Structure 97 | * 98 | * Reference: https://en.wikipedia.org/wiki/Machine-readable_passport 99 | * 100 | */ 101 | protected function isValid() 102 | { 103 | return $this->isPassport() || $this->isVisa() || $this->isTravelDocument1() || $this->isTravelDocument2(); 104 | } 105 | 106 | /** 107 | * Validate Machine Readable Zone from Document 108 | */ 109 | public function validate(): mixed 110 | { 111 | $this->setProperties(); 112 | 113 | if (! $this->isValid()) { 114 | throw new InvalidFormatException("The given input format is invalid!"); 115 | } 116 | 117 | return $this->documentType; 118 | } 119 | } 120 | --------------------------------------------------------------------------------