├── src ├── CityCodeDecoders │ ├── CityDecoderInterface.php │ ├── CompositeCitiesList.php │ ├── ISTATRemoteCSVList.php │ └── InternationalCitiesStaticList.php ├── Exceptions │ ├── CodiceFiscaleGenerationException.php │ └── CodiceFiscaleValidationException.php ├── Checks │ ├── Check.php │ ├── CheckForWrongSize.php │ ├── CheckForEmptyCode.php │ ├── CheckForBadChars.php │ ├── CheckForOmocodiaChars.php │ └── CheckForWrongCode.php ├── CodiceFiscaleConfig.php ├── Faker │ └── CodiceFiscaleFakerProvider.php ├── CodiceFiscaleServiceProvider.php ├── CodiceFiscaleGenerator.php ├── Validators │ └── CodiceFiscaleValidator.php └── CodiceFiscale.php ├── ISSUE_TEMPLATE.md ├── composer.json ├── config └── codicefiscale.php ├── LICENSE.md ├── lang ├── en │ └── validation.php └── it │ └── validation.php ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── .php-cs-fixer.php └── README.md /src/CityCodeDecoders/CityDecoderInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Detailed description 4 | 5 | Provide a detailed description of the change or addition you are proposing. 6 | 7 | Make it clear if the issue is a bug, an enhancement or just a question. 8 | 9 | ## Context 10 | 11 | Why is this change important to you? How would you use it? 12 | 13 | How can it benefit other users? 14 | 15 | ## Possible implementation 16 | 17 | Not obligatory, but suggest an idea for implementing addition or change. 18 | 19 | ## Your environment 20 | 21 | Include as many relevant details about the environment you experienced the bug in and how to reproduce it. 22 | 23 | * Version used (e.g. PHP 5.6, HHVM 3): 24 | * Operating system and version (e.g. Ubuntu 16.04, Windows 7): 25 | * Link to your project: 26 | * ... 27 | * ... -------------------------------------------------------------------------------- /src/CodiceFiscaleConfig.php: -------------------------------------------------------------------------------- 1 | dateFormat = config('codicefiscale.date-format'); 16 | $this->maleLabel = config('codicefiscale.labels.male'); 17 | $this->femaleLabel = config('codicefiscale.labels.female'); 18 | } 19 | 20 | public function getDateFormat(): ?string 21 | { 22 | return $this->dateFormat; 23 | } 24 | 25 | public function getMaleLabel(): ?string 26 | { 27 | return $this->maleLabel; 28 | } 29 | 30 | public function getFemaleLabel(): ?string 31 | { 32 | return $this->femaleLabel; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exceptions/CodiceFiscaleValidationException.php: -------------------------------------------------------------------------------- 1 | =9.0", 17 | "orchestra/testbench": "^10.1", 18 | "friendsofphp/php-cs-fixer": "^3.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "robertogallea\\LaravelCodiceFiscale\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests/" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "phpunit" 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "robertogallea\\LaravelCodiceFiscale\\CodiceFiscaleServiceProvider" 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Faker/CodiceFiscaleFakerProvider.php: -------------------------------------------------------------------------------- 1 | generator->firstName(); 18 | $lastName ??= $this->generator->lastName(); 19 | $birthDate ??= $this->generator->date(); 20 | $birthPlace ??= static::randomElement(ItalianCitiesStaticList::getList()); 21 | $gender ??= static::randomElement([$cfConfig->getMaleLabel(), $cfConfig->getFemaleLabel()]); 22 | 23 | return CodiceFiscale::generate($firstName, $lastName, $birthDate, $birthPlace, $gender); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/codicefiscale.php: -------------------------------------------------------------------------------- 1 | '\robertogallea\LaravelCodiceFiscale\CityCodeDecoders\InternationalCitiesStaticList', 5 | 6 | // The following parameters are used when using IstatRemoveCSVList city decoder class 7 | // The url where the CSV provided by ISTAT is served (should never change) 8 | 'istat-csv-url' => env('CF_ISTAT_CSV_URL', 'https://www.istat.it/storage/codici-unita-amministrative/Elenco-comuni-italiani.csv'), 9 | 10 | // Cache duration (in seconds) for storing the list downloaded from the CSV 11 | 'cache-duration' => env('CF_CACHE_DURATION', 60 * 60 * 24), 12 | 13 | // When using CompositeCitiesList, you may specify the CityDecoders to merge the results from 14 | 'cities-decoder-list' => [ 15 | // '\robertogallea\LaravelCodiceFiscale\CityCodeDecoders\ISTATRemoteCSVList', 16 | // '\robertogallea\LaravelCodiceFiscale\CityCodeDecoders\InternationalCitiesStaticList', 17 | ], 18 | 19 | // used date format for parsing 20 | 'date-format' => 'Y-m-d', 21 | 22 | // used labels for parsing 23 | 'labels' => [ 24 | 'male' => 'M', 25 | 26 | 'female' => 'F', 27 | ], 28 | 29 | ]; 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andrea Marco Sartori 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. -------------------------------------------------------------------------------- /lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute has a wrong size', 5 | 'no_code' => 'The :attribute is empty', 6 | 'bad_characters' => 'The :attribute contains bad characters', 7 | 'bad_omocodia_char' => 'The :attribute contains bad omocodia characters', 8 | 'wrong_code' => 'The :attribute is not valid', 9 | 'missing_city_code' => 'The :attribute contains a non-existing city code', 10 | 'no_match' => 'The :attribute does not match the given personal information', 11 | 'empty_birthdate' => 'The :attribute cannot be validated against an invalid date of birth', 12 | 'wrong_first_name' => 'The digits in :attribute do not match the provided first name', 13 | 'wrong_last_name' => 'The digits in :attribute do not match the provided last name', 14 | 'wrong_birth_day' => 'The digits in :attribute do not match the provided birth day', 15 | 'wrong_birth_month' => 'The digits in :attribute do not match the provided birth month', 16 | 'wrong_birth_year' => 'The digits in :attribute do not match the provided birth year', 17 | 'wrong_birth_place' => 'The digits in :attribute do not match the provided birthplace', 18 | 'wrong_gender' => 'The digits in :attribute do not match the provided gender', 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/it/validation.php: -------------------------------------------------------------------------------- 1 | 'Il campo :attribute deve essere esattamente di 16 caratteri', 5 | 'no_code' => 'Il campo :attribute è vuoto', 6 | 'bad_characters' => 'Il campo :attribute contiene caratteri non validi', 7 | 'bad_omocodia_char' => 'Il carattere relativo all\'omocodia di :attribute non è valido', 8 | 'wrong_code' => 'Il campo :attribute non è valido', 9 | 'missing_city_code' => 'Il campo :attribute ha un codice del comune non corretto o non esistente', 10 | 'no_match' => 'Il campo :attribute non corrisponde ai dati inseriti', 11 | 'empty_birthdate' => 'Il campo :attribute non può essere validato su una data di nascita non valida', 12 | 'wrong_first_name' => 'Le cifre nel :attribute non corrispondono al nome fornito', 13 | 'wrong_last_name' => 'Le cifre nel :attribute non corrispondono al cognome fornito', 14 | 'wrong_birth_day' => 'Le cifre nel :attribute non corrispondono al giorno di nascita fornito', 15 | 'wrong_birth_month' => 'Le cifre nel :attribute non corrispondono al mese di nascita fornito', 16 | 'wrong_birth_year' => 'Le cifre nel :attribute non corrispondono all\'anno di nascita fornito', 17 | 'wrong_birth_place' => 'Le cifre nel :attribute non corrispondono al città di nascita fornita', 18 | 'wrong_gender' => 'Le cifre nel :attribute non corrispondono al genere fornito', 19 | ]; 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/robertogallea/laravel-codicefiscale). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /src/Checks/CheckForOmocodiaChars.php: -------------------------------------------------------------------------------- 1 | '!', 13 | 'B' => '!', 14 | 'C' => '!', 15 | 'D' => '!', 16 | 'E' => '!', 17 | 'F' => '!', 18 | 'G' => '!', 19 | 'H' => '!', 20 | 'I' => '!', 21 | 'J' => '!', 22 | 'K' => '!', 23 | 'L' => '0', 24 | 'M' => '1', 25 | 'N' => '2', 26 | 'O' => '!', 27 | 'P' => '3', 28 | 'Q' => '4', 29 | 'R' => '5', 30 | 'S' => '6', 31 | 'T' => '7', 32 | 'U' => '8', 33 | 'V' => '9', 34 | 'W' => '!', 35 | 'X' => '!', 36 | 'Y' => '!', 37 | 'Z' => '!', 38 | ]; 39 | 40 | /** 41 | * @throws CodiceFiscaleValidationException 42 | */ 43 | public function check($code): bool 44 | { 45 | $cfArray = str_split($code); 46 | 47 | for ($i = 0; $i < count($this->tabReplacementOmocodia); $i++) { 48 | if ( 49 | (! is_numeric($cfArray[$this->tabReplacementOmocodia[$i]])) && 50 | ($this->tabDecodeOmocodia[$cfArray[$this->tabReplacementOmocodia[$i]]] === '!') 51 | ) { 52 | throw new CodiceFiscaleValidationException( 53 | 'Invalid codice fiscale', 54 | CodiceFiscaleValidationException::BAD_OMOCODIA_CHAR 55 | ); 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | Describe your changes in detail. 6 | 7 | ## Motivation and context 8 | 9 | Why is this change required? What problem does it solve? 10 | 11 | If it fixes an open issue, please link to the issue here (if you write `fixes #num` 12 | or `closes #num`, the issue will be automatically closed when the pull is accepted.) 13 | 14 | ## How has this been tested? 15 | 16 | Please describe in detail how you tested your changes. 17 | 18 | Include details of your testing environment, and the tests you ran to 19 | see how your change affects other areas of the code, etc. 20 | 21 | ## Screenshots (if appropriate) 22 | 23 | ## Types of changes 24 | 25 | What types of changes does your code introduce? Put an `x` in all the boxes that apply: 26 | - [ ] Bug fix (non-breaking change which fixes an issue) 27 | - [ ] New feature (non-breaking change which adds functionality) 28 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 29 | 30 | ## Checklist: 31 | 32 | Go over all the following points, and put an `x` in all the boxes that apply. 33 | 34 | Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). 35 | 36 | - [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. 37 | - [ ] My pull request addresses exactly one patch/feature. 38 | - [ ] I have created a branch for this patch/feature. 39 | - [ ] Each individual commit in the pull request is meaningful. 40 | - [ ] I have added tests to cover my changes. 41 | - [ ] If my change requires a change to the documentation, I have updated it accordingly. 42 | 43 | If you're unsure about any of these, don't hesitate to ask. We're here to help! -------------------------------------------------------------------------------- /src/CodiceFiscaleServiceProvider.php: -------------------------------------------------------------------------------- 1 | bootValidator(); 23 | } 24 | 25 | /** 26 | * Register the application services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->config(); 33 | 34 | $this->translations(); 35 | 36 | $this->app->bind( 37 | CityDecoderInterface::class, 38 | config('codicefiscale.city-decoder') 39 | ); 40 | 41 | $this->registerFakerProvider(); 42 | } 43 | 44 | public function bootValidator() 45 | { 46 | Validator::extend('codice_fiscale', CodiceFiscaleValidator::class); 47 | } 48 | 49 | private function config() 50 | { 51 | $configPath = $this->packagePath('config/codicefiscale.php'); 52 | 53 | $this->publishes([ 54 | $configPath => config_path('codicefiscale.php'), 55 | ], 'config'); 56 | 57 | $this->mergeConfigFrom($configPath, 'codicefiscale'); 58 | } 59 | 60 | private function translations() 61 | { 62 | $translationsPath = $this->packagePath('lang'); 63 | 64 | $this->loadTranslationsFrom($translationsPath, 'codicefiscale'); 65 | 66 | $this->publishes([ 67 | $translationsPath => $this->app->langPath('vendor/codicefiscale'), 68 | ], 'lang'); 69 | } 70 | 71 | private function packagePath($path) 72 | { 73 | return __DIR__."/../$path"; 74 | } 75 | 76 | public function registerFakerProvider(): void 77 | { 78 | if (! class_exists(Factory::class)) { 79 | return; 80 | } 81 | 82 | $this->app->singleton(Generator::class, function () { 83 | $faker = Factory::create(); 84 | $faker->addProvider(new CodiceFiscaleFakerProvider($faker)); 85 | 86 | return $faker; 87 | }); 88 | 89 | if (function_exists('fake')) { 90 | fake()->addProvider(app(CodiceFiscaleFakerProvider::class)); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/CityCodeDecoders/ISTATRemoteCSVList.php: -------------------------------------------------------------------------------- 1 | 'S\'', 'š' => 's\'', 'Ž' => 'Z\'', 'ž' => 'z\'', 'À' => 'A\'', 'Á' => 'A\'', 'Â' => 'A\'', 'Ã' => 'A\'', 'Ä' => 'A\'', 'Å' => 'A\'', 'Æ' => 'A\'', 'Ç' => 'C\'', 'È' => 'E\'', 'É' => 'E\'', 11 | 'Ê' => 'E\'', 'Ë' => 'E\'', 'Ì' => 'I\'', 'Í' => 'I\'', 'Î' => 'I\'', 'Ï' => 'I\'', 'Ñ' => 'N\'', 'Ò' => 'O\'', 'Ó' => 'O\'', 'Ô' => 'O\'', 'Õ' => 'O\'', 'Ö' => 'O\'', 'Ø' => 'O\'', 'Ù' => 'U\'', 12 | 'Ú' => 'U\'', 'Û' => 'U\'', 'Ü' => 'U\'', 'Ý' => 'Y\'', 'Þ' => 'B\'', 'ß' => 'Ss\'', 'à' => 'a\'', 'á' => 'a\'', 'â' => 'a\'', 'ã' => 'a\'', 'ä' => 'a\'', 'å' => 'a\'', 'æ' => 'a\'', 'ç' => 'c\'', 13 | 'è' => 'e\'', 'é' => 'e\'', 'ê' => 'e\'', 'ë' => 'e\'', 'ì' => 'i\'', 'í' => 'i\'', 'î' => 'i\'', 'ï' => 'i\'', 'ð' => 'o\'', 'ñ' => 'n\'', 'ò' => 'o\'', 'ó' => 'o\'', 'ô' => 'o\'', 'õ' => 'o\'', 14 | 'ö' => 'o\'', 'ø' => 'o\'', 'ù' => 'u\'', 'ú' => 'u\'', 'û' => 'u\'', 'ý' => 'y\'', 'þ' => 'b\'', 'ÿ' => 'y', 15 | ]; 16 | 17 | public static function getList() 18 | { 19 | return \Cache::remember('cities-list', config('codicefiscale.cache-duration'), function () { 20 | $client = new Client(['verify' => false]); 21 | $response = $client->request('GET', config('codicefiscale.istat-csv-url')); 22 | 23 | $body = iconv('ISO-8859-1', 'UTF-8', $response->getBody()); 24 | 25 | $body = self::str_replace_times("\n", '', $body, 2); 26 | 27 | $data = self::getCsv($body); 28 | $list = self::csvToList($data); 29 | 30 | return $list; 31 | }); 32 | } 33 | 34 | private static function str_replace_times($from, $to, $content, $times) 35 | { 36 | $from = '/'.preg_quote($from, '/').'/'; 37 | 38 | return preg_replace($from, $to, $content, $times); 39 | } 40 | 41 | private static function getCsv(string $body) 42 | { 43 | $data = str_getcsv($body, "\n"); 44 | 45 | foreach ($data as &$row) { 46 | $row = str_getcsv($row, ';'); 47 | } 48 | 49 | return $data; 50 | } 51 | 52 | private static function csvToList(array $data) 53 | { 54 | unset($data[0]); 55 | $newData = []; 56 | 57 | $data = array_walk($data, function ($row) use (&$newData) { 58 | $newData[$row[19]] = strtoupper(strtr($row[6], self::accentTable)); 59 | }); 60 | 61 | return $newData; 62 | } 63 | 64 | public function flushCache() 65 | { 66 | cache()->forget('cities-list'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `andrea.marco.sartori@gmail.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /src/Checks/CheckForWrongCode.php: -------------------------------------------------------------------------------- 1 | 0, 13 | '1' => 1, 14 | '2' => 2, 15 | '3' => 3, 16 | '4' => 4, 17 | '5' => 5, 18 | '6' => 6, 19 | '7' => 7, 20 | '8' => 8, 21 | '9' => 9, 22 | 'A' => 0, 23 | 'B' => 1, 24 | 'C' => 2, 25 | 'D' => 3, 26 | 'E' => 4, 27 | 'F' => 5, 28 | 'G' => 6, 29 | 'H' => 7, 30 | 'I' => 8, 31 | 'J' => 9, 32 | 'K' => 10, 33 | 'L' => 11, 34 | 'M' => 12, 35 | 'N' => 13, 36 | 'O' => 14, 37 | 'P' => 15, 38 | 'Q' => 16, 39 | 'R' => 17, 40 | 'S' => 18, 41 | 'T' => 19, 42 | 'U' => 20, 43 | 'V' => 21, 44 | 'W' => 22, 45 | 'X' => 23, 46 | 'Y' => 24, 47 | 'Z' => 25, 48 | ]; 49 | 50 | protected $tabOddChars = [ 51 | '0' => 1, 52 | '1' => 0, 53 | '2' => 5, 54 | '3' => 7, 55 | '4' => 9, 56 | '5' => 13, 57 | '6' => 15, 58 | '7' => 17, 59 | '8' => 19, 60 | '9' => 21, 61 | 'A' => 1, 62 | 'B' => 0, 63 | 'C' => 5, 64 | 'D' => 7, 65 | 'E' => 9, 66 | 'F' => 13, 67 | 'G' => 15, 68 | 'H' => 17, 69 | 'I' => 19, 70 | 'J' => 21, 71 | 'K' => 2, 72 | 'L' => 4, 73 | 'M' => 18, 74 | 'N' => 20, 75 | 'O' => 11, 76 | 'P' => 3, 77 | 'Q' => 6, 78 | 'R' => 8, 79 | 'S' => 12, 80 | 'T' => 14, 81 | 'U' => 16, 82 | 'V' => 10, 83 | 'W' => 22, 84 | 'X' => 25, 85 | 'Y' => 24, 86 | 'Z' => 23, 87 | ]; 88 | 89 | protected $tabControlCode = [ 90 | 0 => 'A', 91 | 1 => 'B', 92 | 2 => 'C', 93 | 3 => 'D', 94 | 4 => 'E', 95 | 5 => 'F', 96 | 6 => 'G', 97 | 7 => 'H', 98 | 8 => 'I', 99 | 9 => 'J', 100 | 10 => 'K', 101 | 11 => 'L', 102 | 12 => 'M', 103 | 13 => 'N', 104 | 14 => 'O', 105 | 15 => 'P', 106 | 16 => 'Q', 107 | 17 => 'R', 108 | 18 => 'S', 109 | 19 => 'T', 110 | 20 => 'U', 111 | 21 => 'V', 112 | 22 => 'W', 113 | 23 => 'X', 114 | 24 => 'Y', 115 | 25 => 'Z', 116 | ]; 117 | 118 | /** 119 | * @throws CodiceFiscaleValidationException 120 | */ 121 | public function check($code): bool 122 | { 123 | $cfArray = str_split($code); 124 | 125 | $even = 0; 126 | $odd = $this->tabOddChars[$cfArray[14]]; 127 | 128 | for ($i = 0; $i < 13; $i += 2) { 129 | $odd = $odd + $this->tabOddChars[$cfArray[$i]]; 130 | $even = $even + $this->tabEvenChars[$cfArray[$i + 1]]; 131 | } 132 | 133 | if (! ($this->tabControlCode[($even + $odd) % 26] === $cfArray[15]) || (! $this->checkRegex($code))) { 134 | throw new CodiceFiscaleValidationException( 135 | 'Invalid codice fiscale', 136 | CodiceFiscaleValidationException::WRONG_CODE 137 | ); 138 | } 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * @param string $code 145 | * 146 | * @return bool 147 | */ 148 | protected function checkRegex(string $code): bool 149 | { 150 | return (bool) preg_match(self::CF_REGEX, $code); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'binary_operator_spaces' => [ 10 | 'default' => 'single_space', 11 | 'operators' => ['=>' => null], 12 | ], 13 | 'blank_line_after_namespace' => true, 14 | 'blank_line_after_opening_tag' => true, 15 | 'blank_line_before_statement' => [ 16 | 'statements' => ['return'], 17 | ], 18 | 'braces' => true, 19 | 'cast_spaces' => true, 20 | 'class_attributes_separation' => [ 21 | 'elements' => [ 22 | 'const' => 'one', 23 | 'method' => 'one', 24 | 'property' => 'one', 25 | 'trait_import' => 'none', 26 | ], 27 | ], 28 | 'class_definition' => [ 29 | 'multi_line_extends_each_single_line' => true, 30 | 'single_item_single_line' => true, 31 | 'single_line' => true, 32 | ], 33 | 'clean_namespace' => true, 34 | 'compact_nullable_typehint' => true, 35 | 'concat_space' => [ 36 | 'spacing' => 'none', 37 | ], 38 | 'constant_case' => ['case' => 'lower'], 39 | 'declare_equal_normalize' => true, 40 | 'elseif' => true, 41 | 'encoding' => true, 42 | 'full_opening_tag' => true, 43 | 'fully_qualified_strict_types' => true, 44 | 'function_declaration' => true, 45 | 'function_typehint_space' => true, 46 | 'general_phpdoc_tag_rename' => true, 47 | 'heredoc_to_nowdoc' => true, 48 | 'include' => true, 49 | 'increment_style' => ['style' => 'post'], 50 | 'indentation_type' => true, 51 | 'integer_literal_case' => true, 52 | 'lambda_not_used_import' => true, 53 | 'linebreak_after_opening_tag' => true, 54 | 'line_ending' => true, 55 | 'lowercase_cast' => true, 56 | 'lowercase_keywords' => true, 57 | 'lowercase_static_reference' => true, 58 | 'magic_method_casing' => true, 59 | 'magic_constant_casing' => true, 60 | 'method_argument_space' => [ 61 | 'on_multiline' => 'ignore', 62 | ], 63 | 'multiline_whitespace_before_semicolons' => [ 64 | 'strategy' => 'no_multi_line', 65 | ], 66 | 'native_function_casing' => true, 67 | 'native_function_type_declaration_casing' => true, 68 | 'no_alias_functions' => true, 69 | 'no_alias_language_construct_call' => true, 70 | 'no_alternative_syntax' => true, 71 | 'no_binary_string' => true, 72 | 'no_blank_lines_after_class_opening' => true, 73 | 'no_blank_lines_after_phpdoc' => true, 74 | 'no_closing_tag' => true, 75 | 'no_empty_phpdoc' => true, 76 | 'no_empty_statement' => true, 77 | 'no_extra_blank_lines' => [ 78 | 'tokens' => [ 79 | 'extra', 80 | 'throw', 81 | 'use', 82 | ], 83 | ], 84 | 'no_leading_import_slash' => true, 85 | 'no_leading_namespace_whitespace' => true, 86 | 'no_mixed_echo_print' => [ 87 | 'use' => 'echo', 88 | ], 89 | 'no_multiline_whitespace_around_double_arrow' => true, 90 | 'no_short_bool_cast' => true, 91 | 'no_singleline_whitespace_before_semicolons' => true, 92 | 'no_spaces_after_function_name' => true, 93 | 'no_space_around_double_colon' => true, 94 | 'no_spaces_around_offset' => [ 95 | 'positions' => ['inside', 'outside'], 96 | ], 97 | 'no_spaces_inside_parenthesis' => true, 98 | 'no_trailing_comma_in_list_call' => true, 99 | 'no_trailing_comma_in_singleline_array' => true, 100 | 'no_trailing_whitespace' => true, 101 | 'no_trailing_whitespace_in_comment' => true, 102 | 'no_unneeded_control_parentheses' => [ 103 | 'statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield'], 104 | ], 105 | 'no_unneeded_curly_braces' => true, 106 | 'no_unset_cast' => true, 107 | 'no_unused_imports' => true, 108 | 'no_unreachable_default_argument_value' => true, 109 | 'no_useless_return' => true, 110 | 'no_whitespace_before_comma_in_array' => true, 111 | 'no_whitespace_in_blank_line' => true, 112 | 'normalize_index_brace' => true, 113 | 'not_operator_with_successor_space' => true, 114 | 'object_operator_without_whitespace' => true, 115 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 116 | 'psr_autoloading' => false, 117 | 'phpdoc_indent' => true, 118 | 'phpdoc_inline_tag_normalizer' => true, 119 | 'phpdoc_no_access' => true, 120 | 'phpdoc_no_package' => true, 121 | 'phpdoc_no_useless_inheritdoc' => true, 122 | 'phpdoc_scalar' => true, 123 | 'phpdoc_single_line_var_spacing' => true, 124 | 'phpdoc_summary' => false, 125 | 'phpdoc_to_comment' => false, 126 | 'phpdoc_tag_type' => true, 127 | 'phpdoc_trim' => true, 128 | 'phpdoc_types' => true, 129 | 'phpdoc_var_without_name' => true, 130 | 'return_type_declaration' => ['space_before' => 'none'], 131 | 'self_accessor' => true, 132 | 'short_scalar_cast' => true, 133 | 'simplified_null_return' => false, 134 | 'single_blank_line_at_eof' => true, 135 | 'single_blank_line_before_namespace' => true, 136 | 'single_class_element_per_statement' => [ 137 | 'elements' => ['const', 'property'], 138 | ], 139 | 'single_import_per_statement' => true, 140 | 'single_line_after_imports' => true, 141 | 'single_line_comment_style' => [ 142 | 'comment_types' => ['hash'], 143 | ], 144 | 'single_quote' => true, 145 | 'space_after_semicolon' => true, 146 | 'standardize_not_equals' => true, 147 | 'switch_case_semicolon_to_colon' => true, 148 | 'switch_case_space' => true, 149 | 'ternary_operator_spaces' => true, 150 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 151 | 'trim_array_spaces' => true, 152 | 'unary_operator_spaces' => true, 153 | 'visibility_required' => [ 154 | 'elements' => ['method', 'property'], 155 | ], 156 | 'whitespace_after_comma_in_array' => true, 157 | ]; 158 | 159 | $finder = Finder::create() 160 | ->in([ 161 | __DIR__.'/src', 162 | __DIR__.'/lang', 163 | __DIR__.'/config', 164 | __DIR__.'/tests', 165 | ]) 166 | ->name('*.php') 167 | ->notName('*.blade.php') 168 | ->ignoreDotFiles(true) 169 | ->ignoreVCS(true); 170 | 171 | return (new Config()) 172 | ->setFinder($finder) 173 | ->setRules($rules) 174 | ->setRiskyAllowed(true) 175 | ->setUsingCache(true); 176 | -------------------------------------------------------------------------------- /src/CodiceFiscaleGenerator.php: -------------------------------------------------------------------------------- 1 | 'A', 2 => 'B', 3 => 'C', 4 => 'D', 5 => 'E', 27 | 6 => 'H', 7 => 'L', 8 => 'M', 9 => 'P', 10 => 'R', 28 | 11 => 'S', 12 => 'T', 29 | ]; 30 | 31 | protected array $_pari = [ 32 | '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, 33 | '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, 34 | 'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 35 | 'F' => 5, 'G' => 6, 'H' => 7, 'I' => 8, 'J' => 9, 36 | 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 37 | 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 38 | 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 39 | 'Z' => 25, 40 | ]; 41 | 42 | protected array $_dispari = [ 43 | '0' => 1, '1' => 0, '2' => 5, '3' => 7, '4' => 9, 44 | '5' => 13, '6' => 15, '7' => 17, '8' => 19, '9' => 21, 45 | 'A' => 1, 'B' => 0, 'C' => 5, 'D' => 7, 'E' => 9, 46 | 'F' => 13, 'G' => 15, 'H' => 17, 'I' => 19, 'J' => 21, 47 | 'K' => 2, 'L' => 4, 'M' => 18, 'N' => 20, 'O' => 11, 48 | 'P' => 3, 'Q' => 6, 'R' => 8, 'S' => 12, 'T' => 14, 49 | 'U' => 16, 'V' => 10, 'W' => 22, 'X' => 25, 'Y' => 24, 50 | 'Z' => 23, 51 | ]; 52 | 53 | protected array $_controllo = [ 54 | '0' => 'A', '1' => 'B', '2' => 'C', '3' => 'D', 55 | '4' => 'E', '5' => 'F', '6' => 'G', '7' => 'H', 56 | '8' => 'I', '9' => 'J', '10' => 'K', '11' => 'L', 57 | '12' => 'M', '13' => 'N', '14' => 'O', '15' => 'P', 58 | '16' => 'Q', '17' => 'R', '18' => 'S', '19' => 'T', 59 | '20' => 'U', '21' => 'V', '22' => 'W', '23' => 'X', 60 | '24' => 'Y', '25' => 'Z', 61 | ]; 62 | 63 | public function __construct(CityDecoderInterface $cityDecoder, CodiceFiscaleConfig $config) 64 | { 65 | $this->cityDecoder = $cityDecoder; 66 | $this->config = $config; 67 | } 68 | 69 | protected function _scomponi($string, array $haystack) 70 | { 71 | $letters = []; 72 | foreach (str_split($string) as $needle) { 73 | if (in_array($needle, $haystack)) { 74 | $letters[] = $needle; 75 | } 76 | } 77 | 78 | return $letters; 79 | } 80 | 81 | protected function _trovaVocali($string) 82 | { 83 | return $this->_scomponi($string, $this->_vocali); 84 | } 85 | 86 | protected function _trovaConsonanti($string) 87 | { 88 | return $this->_scomponi($string, $this->_consonanti); 89 | } 90 | 91 | protected function _pulisci($string, $toupper = true) 92 | { 93 | $result = preg_replace('/[^A-Za-z]*/', '', $string); 94 | 95 | return ($toupper) ? strtoupper($result) : $result; 96 | } 97 | 98 | protected function _aggiungiX($string) 99 | { 100 | $code = $string; 101 | while (strlen($code) < 3) { 102 | $code .= 'X'; 103 | } 104 | 105 | return $code; 106 | } 107 | 108 | protected function _calcolaNome() 109 | { 110 | $code = ''; 111 | if (! $this->nome) { 112 | throw new CodiceFiscaleGenerationException('First name not enetered'); 113 | } 114 | $nome = $this->_pulisci($this->nome); 115 | 116 | if (strlen($nome) < 3) { 117 | return $this->_aggiungiX($nome); 118 | } 119 | $nome_cons = $this->_trovaConsonanti($nome); 120 | 121 | if (count($nome_cons) <= 3) { 122 | $code = implode('', $nome_cons); 123 | } else { 124 | for ($i = 0; $i < 4; $i++) { 125 | if ($i == 1) { 126 | continue; 127 | } 128 | if (! empty($nome_cons[$i])) { 129 | $code .= $nome_cons[$i]; 130 | } 131 | } 132 | } 133 | 134 | if (strlen($code) < 3) { 135 | $nome_voc = $this->_trovaVocali($nome); 136 | while (strlen($code) < 3) { 137 | $code .= array_shift($nome_voc); 138 | } 139 | } 140 | 141 | return $code; 142 | } 143 | 144 | protected function _calcolaCognome() 145 | { 146 | if (! $this->cognome) { 147 | throw new CodiceFiscaleGenerationException('Last name not entered'); 148 | } 149 | $cognome = $this->_pulisci($this->cognome); 150 | 151 | if (strlen($cognome) < 3) { 152 | return $this->_aggiungiX($cognome); 153 | } 154 | $cognome_cons = $this->_trovaConsonanti($cognome); 155 | 156 | $code = ''; 157 | for ($i = 0; $i < 3; $i++) { 158 | if (array_key_exists($i, $cognome_cons)) { 159 | $code .= $cognome_cons[$i]; 160 | } 161 | } 162 | 163 | if (strlen($code) < 3) { 164 | $cognome_voc = $this->_trovaVocali($cognome); 165 | while (strlen($code) < 3) { 166 | $code .= array_shift($cognome_voc); 167 | } 168 | } 169 | 170 | return $code; 171 | } 172 | 173 | protected function _calcolaDataNascita() 174 | { 175 | if (! $this->data) { 176 | throw new CodiceFiscaleGenerationException('Birth date not entered'); 177 | } 178 | 179 | if (! $this->sesso) { 180 | throw new CodiceFiscaleGenerationException('Geneder not entered'); 181 | } 182 | 183 | if ($this->data instanceof Carbon) { 184 | $data = $this->data; 185 | } else { 186 | $data = Carbon::createFromFormat($this->config->getDateFormat(), $this->data); 187 | } 188 | 189 | $giorno = $data->format('j'); 190 | $mese = $data->format('n'); 191 | $anno = $data->format('Y'); 192 | 193 | $aa = substr($anno, -2); 194 | 195 | $mm = $this->_mesi[$mese]; 196 | 197 | $gg = ($this->sesso == config('codicefiscale.labels.male')) ? $giorno : $giorno + 40; 198 | $gg = str_pad($gg, 2, '0', STR_PAD_LEFT); 199 | 200 | return $aa.$mm.$gg; 201 | } 202 | 203 | protected function _calcolaCatastale() 204 | { 205 | $place = strtoupper($this->comune); 206 | if (array_key_exists($place, $this->cityDecoder->getList())) { 207 | $place_code = $place; 208 | } else { 209 | $place_code = array_search($place, $this->cityDecoder->getList()); 210 | if (! $place_code) { 211 | throw new CodiceFiscaleGenerationException('Birth place must be a valid city code or name'); 212 | } 213 | } 214 | 215 | return $place_code; 216 | } 217 | 218 | protected function _calcolaCifraControllo($codice) 219 | { 220 | $code = str_split($codice); 221 | $sum = 0; 222 | for ($i = 1; $i <= count($code); $i++) { 223 | $cifra = $code[$i - 1]; 224 | $sum += ($i % 2) ? $this->_dispari[$cifra] : $this->_pari[$cifra]; 225 | } 226 | $sum %= 26; 227 | 228 | return $this->_controllo[$sum]; 229 | } 230 | 231 | public function calcola() 232 | { 233 | $codice = $this->_calcolaCognome(). 234 | $this->_calcolaNome(). 235 | $this->_calcolaDataNascita(). 236 | $this->_calcolaCatastale(); 237 | $codice .= $this->_calcolaCifraControllo($codice); 238 | if (strlen($codice) != 16) { 239 | throw new CodiceFiscaleGenerationException('Generated code has not 16 digits: '.$codice); 240 | } 241 | 242 | return $codice; 243 | } 244 | 245 | public function __set($key, $value) 246 | { 247 | $this->_parametri[$key] = $value; 248 | } 249 | 250 | public function __get($key) 251 | { 252 | return $this->_parametri[$key]; 253 | } 254 | 255 | public function _isset($key) 256 | { 257 | return isset($this->_parametri[$key]); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Validators/CodiceFiscaleValidator.php: -------------------------------------------------------------------------------- 1 | codiceFiscale = $codiceFiscale; 16 | } 17 | 18 | public function validate($attribute, $value, $parameters, $validator): bool 19 | { 20 | $errorCodes = [ 21 | 'first_name' => CodiceFiscaleValidationException::WRONG_FIRST_NAME, 22 | 'last_name' => CodiceFiscaleValidationException::WRONG_LAST_NAME, 23 | 'day' => CodiceFiscaleValidationException::WRONG_BIRTH_DAY, 24 | 'month' => CodiceFiscaleValidationException::WRONG_BIRTH_MONTH, 25 | 'year' => CodiceFiscaleValidationException::WRONG_BIRTH_YEAR, 26 | 'place' => CodiceFiscaleValidationException::WRONG_BIRTH_PLACE, 27 | 'gender' => CodiceFiscaleValidationException::WRONG_GENDER, 28 | ]; 29 | 30 | try { 31 | $this->codiceFiscale->parse($value); 32 | $data = $validator->getData(); 33 | 34 | if (count($parameters)) { 35 | $pieces = [ 36 | 'first_name' => '', 37 | 'last_name' => '', 38 | 'birthdate' => '', 39 | 'place' => '', 40 | 'gender' => '', 41 | ]; 42 | 43 | foreach ($parameters as $parameter) { 44 | $pair = explode('=', $parameter); 45 | $pieces[$pair[0]] = $data[$pair[1]] ?? ''; 46 | } 47 | 48 | try { 49 | $cf = CodiceFiscale::generate(...array_values($pieces)); 50 | } catch (CodiceFiscaleGenerationException $exception) { 51 | throw new CodiceFiscaleValidationException( 52 | 'Invalid codice fiscale', 53 | CodiceFiscaleValidationException::NO_MATCH 54 | ); 55 | } catch (\Exception $exception) { 56 | throw new CodiceFiscaleValidationException( 57 | 'Invalid codice fiscale', 58 | CodiceFiscaleValidationException::EMPTY_BIRTHDATE 59 | ); 60 | } 61 | 62 | if ($value != $cf) { 63 | $newCodiceFiscale = new CodiceFiscale(); 64 | $newCodiceFiscale->parse($cf); 65 | $this->compareAttribute('First Name', $newCodiceFiscale->getFirstName(), $this->codiceFiscale->getFirstName(), $errorCodes['first_name']); 66 | $this->compareAttribute('Last Name', $newCodiceFiscale->getLastName(), $this->codiceFiscale->getLastName(), $errorCodes['last_name']); 67 | $this->compareAttribute('Day', $newCodiceFiscale->getDay(), $this->codiceFiscale->getDay(), $errorCodes['day']); 68 | $this->compareAttribute('Month', $newCodiceFiscale->getMonth(), $this->codiceFiscale->getMonth(), $errorCodes['month']); 69 | $this->compareAttribute('Year', $newCodiceFiscale->getYear(), $this->codiceFiscale->getYear(), $errorCodes['year']); 70 | $this->compareAttribute('Birth Place', $newCodiceFiscale->getBirthPlace(), $this->codiceFiscale->getBirthPlace(), $errorCodes['place']); 71 | $this->compareAttribute('Gender', $newCodiceFiscale->getGender(), $this->codiceFiscale->getGender(), $errorCodes['gender']); 72 | throw new CodiceFiscaleValidationException( 73 | 'Invalid codice fiscale', 74 | CodiceFiscaleValidationException::NO_MATCH 75 | ); 76 | } 77 | } 78 | } catch (CodiceFiscaleValidationException $exception) { 79 | switch ($exception->getCode()) { 80 | case CodiceFiscaleValidationException::NO_CODE: 81 | $error_msg = trans('codicefiscale::validation.no_code', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 82 | break; 83 | case CodiceFiscaleValidationException::WRONG_SIZE: 84 | $error_msg = trans('codicefiscale::validation.wrong_size', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 85 | break; 86 | case CodiceFiscaleValidationException::BAD_CHARACTERS: 87 | $error_msg = trans('codicefiscale::validation.bad_characters', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 88 | break; 89 | case CodiceFiscaleValidationException::BAD_OMOCODIA_CHAR: 90 | $error_msg = trans('codicefiscale::validation.bad_omocodia_char', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 91 | break; 92 | case CodiceFiscaleValidationException::WRONG_CODE: 93 | $error_msg = trans('codicefiscale::validation.wrong_code', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 94 | break; 95 | case CodiceFiscaleValidationException::MISSING_CITY_CODE: 96 | $error_msg = trans('codicefiscale::validation.missing_city_code', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 97 | break; 98 | case CodiceFiscaleValidationException::NO_MATCH: 99 | $error_msg = trans('codicefiscale::validation.no_match', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 100 | break; 101 | case CodiceFiscaleValidationException::EMPTY_BIRTHDATE: 102 | $error_msg = trans('codicefiscale::validation.empty_birthdate', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 103 | break; 104 | case CodiceFiscaleValidationException::WRONG_FIRST_NAME: 105 | $error_msg = trans('codicefiscale::validation.wrong_first_name', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 106 | break; 107 | case CodiceFiscaleValidationException::WRONG_LAST_NAME: 108 | $error_msg = trans('codicefiscale::validation.wrong_last_name', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 109 | break; 110 | case CodiceFiscaleValidationException::WRONG_BIRTH_DAY: 111 | $error_msg = trans('codicefiscale::validation.wrong_birth_day', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 112 | break; 113 | case CodiceFiscaleValidationException::WRONG_BIRTH_MONTH: 114 | $error_msg = trans('codicefiscale::validation.wrong_birth_month', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 115 | break; 116 | case CodiceFiscaleValidationException::WRONG_BIRTH_YEAR: 117 | $error_msg = trans('codicefiscale::validation.wrong_birth_year', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 118 | break; 119 | case CodiceFiscaleValidationException::WRONG_BIRTH_PLACE: 120 | $error_msg = trans('codicefiscale::validation.wrong_birth_place', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 121 | break; 122 | case CodiceFiscaleValidationException::WRONG_GENDER: 123 | $error_msg = trans('codicefiscale::validation.wrong_gender', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 124 | break; 125 | default: 126 | $error_msg = trans('codicefiscale::validation.wrong_code', ['attribute' => $validator->getDisplayableAttribute($attribute)]); 127 | } 128 | 129 | $validator->addReplacer('codice_fiscale', function ($message, $attribute, $rule, $parameters, $validator) use ($error_msg) { 130 | return str_replace([':attribute'], [$validator->getDisplayableAttribute($attribute)], str_replace('codice fiscale', ':attribute', $error_msg)); 131 | }); 132 | 133 | return false; 134 | } 135 | 136 | return true; 137 | } 138 | 139 | public function compareAttribute($attribute, $new, $old, $error): void 140 | { 141 | if ($new != $old) { 142 | throw new CodiceFiscaleValidationException( 143 | "Invalid $attribute", 144 | $error 145 | ); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/CodiceFiscale.php: -------------------------------------------------------------------------------- 1 | config = $config ?? resolve(CodiceFiscaleConfig::class); 53 | 54 | $this->cityDecoder = $cityDecoder ?? resolve(CityDecoderInterface::class); 55 | 56 | $this->tabReplacementOmocodia = [6, 7, 9, 10, 12, 13, 14]; 57 | 58 | $this->tabDecodeOmocodia = [ 59 | 'A' => '!', 60 | 'B' => '!', 61 | 'C' => '!', 62 | 'D' => '!', 63 | 'E' => '!', 64 | 'F' => '!', 65 | 'G' => '!', 66 | 'H' => '!', 67 | 'I' => '!', 68 | 'J' => '!', 69 | 'K' => '!', 70 | 'L' => '0', 71 | 'M' => '1', 72 | 'N' => '2', 73 | 'O' => '!', 74 | 'P' => '3', 75 | 'Q' => '4', 76 | 'R' => '5', 77 | 'S' => '6', 78 | 'T' => '7', 79 | 'U' => '8', 80 | 'V' => '9', 81 | 'W' => '!', 82 | 'X' => '!', 83 | 'Y' => '!', 84 | 'Z' => '!', 85 | ]; 86 | 87 | $this->tabDecodeMonths = [ 88 | 'A' => '01', 89 | 'B' => '02', 90 | 'C' => '03', 91 | 'D' => '04', 92 | 'E' => '05', 93 | 'H' => '06', 94 | 'L' => '07', 95 | 'M' => '08', 96 | 'P' => '09', 97 | 'R' => '10', 98 | 'S' => '11', 99 | 'T' => '12', 100 | ]; 101 | } 102 | 103 | public static function generate(string $first_name, string $last_name, Carbon|string $birth_date, string $place, string $gender, ?CodiceFiscaleConfig $config = null): string 104 | { 105 | $config = $config ?: resolve(CodiceFiscaleConfig::class); 106 | 107 | $cf_gen = resolve(CodiceFiscaleGenerator::class); 108 | 109 | $cf_gen->nome = $first_name; 110 | $cf_gen->cognome = $last_name; 111 | 112 | $cf_gen->comune = $place; 113 | $cf_gen->sesso = $gender; 114 | 115 | if ($birth_date instanceof Carbon) { 116 | $date = $birth_date; 117 | } else { 118 | $date = Carbon::createFromFormat($config->getDateFormat(), $birth_date); 119 | } 120 | $cf_gen->data = $date; 121 | 122 | return $cf_gen->calcola(); 123 | } 124 | 125 | /** 126 | * @returns bool 127 | */ 128 | public function tryParse(string $cf): bool 129 | { 130 | try { 131 | $this->parse($cf); 132 | 133 | return true; 134 | } catch (\Exception $ex) { 135 | $this->error = $ex; 136 | 137 | return false; 138 | } 139 | } 140 | 141 | /** 142 | * @returns array 143 | * 144 | * @throws CodiceFiscaleValidationException 145 | */ 146 | public function parse(?string $cf): array 147 | { 148 | $cf = strtoupper($cf); 149 | $this->cf = $cf; 150 | $this->isValid = null; 151 | $this->gender = null; 152 | $this->birthPlace = null; 153 | $this->day = null; 154 | $this->month = null; 155 | $this->year = null; 156 | $this->error = null; 157 | 158 | foreach ($this->checks as $check) { 159 | (new $check())->check($cf); 160 | } 161 | 162 | $cfArray = str_split($cf); 163 | 164 | for ($i = 0; $i < count($this->tabReplacementOmocodia); $i++) { 165 | if (! is_numeric($cfArray[$this->tabReplacementOmocodia[$i]])) { 166 | $cfArray[$this->tabReplacementOmocodia[$i]] = 167 | $this->tabDecodeOmocodia[$cfArray[$this->tabReplacementOmocodia[$i]]]; 168 | } 169 | } 170 | 171 | $adaptedCF = implode($cfArray); 172 | 173 | $this->isValid = true; 174 | 175 | $this->gender = (substr($adaptedCF, 9, 2) > '40' ? $this->config->getFemaleLabel() : $this->config->getMaleLabel()); 176 | 177 | $this->birthPlace = substr($adaptedCF, 11, 4); 178 | $this->year = substr($adaptedCF, 6, 2); 179 | $monthPart = substr($adaptedCF, 8, 1); 180 | $this->month = array_key_exists($monthPart, $this->tabDecodeMonths) ? $this->tabDecodeMonths[$monthPart] : null; 181 | 182 | $this->day = substr($adaptedCF, 9, 2); 183 | 184 | if ($this->gender === $this->config->getFemaleLabel()) { 185 | $this->day = $this->day - 40; 186 | if (strlen($this->day) === 1) { 187 | $this->day = '0'.$this->day; 188 | } 189 | } 190 | 191 | return $this->asArray(); 192 | } 193 | 194 | public function isValid(): bool 195 | { 196 | return $this->isValid ?? false; 197 | } 198 | 199 | public function getError(): ?\Exception 200 | { 201 | return $this->error; 202 | } 203 | 204 | public function getGender(): ?string 205 | { 206 | return $this->gender; 207 | } 208 | 209 | public function getBirthPlace(): ?string 210 | { 211 | return $this->birthPlace; 212 | } 213 | 214 | /** 215 | * Determines if the codice fiscale refers to a foreign birthplace. 216 | * Rule: rely solely on getBirthPlace(): if it starts with 'Z' then it's international. 217 | */ 218 | public function isInternational(): bool 219 | { 220 | $place = $this->getBirthPlace(); 221 | if (! is_string($place) || $place === '') { 222 | return false; 223 | } 224 | 225 | return strtoupper($place[0]) === 'Z'; 226 | } 227 | 228 | /** 229 | * Determines if the codice fiscale refers to an Italian birthplace. 230 | * This is the logical negation of isInternational(). 231 | */ 232 | public function isItalian(): bool 233 | { 234 | return ! $this->isInternational(); 235 | } 236 | 237 | public function getBirthPlaceComplete(): ?string 238 | { 239 | if ($this->getBirthPlace() === null) { 240 | return null; 241 | } 242 | 243 | if (! array_key_exists($this->getBirthPlace(), $this->cityDecoder->getList())) { 244 | throw new CodiceFiscaleValidationException( 245 | 'Invalid codice fiscale', 246 | CodiceFiscaleValidationException::MISSING_CITY_CODE 247 | ); 248 | } 249 | 250 | return ucwords(strtolower($this->cityDecoder->getList()[$this->getBirthPlace()])); 251 | } 252 | 253 | public function getBirthdate(): Carbon 254 | { 255 | try { 256 | return Carbon::parse($this->getYear().'-'.$this->getMonth().'-'.$this->getDay()); 257 | } catch (\Exception $exception) { 258 | throw new CodiceFiscaleValidationException('Parsed date is not valid'); 259 | } 260 | } 261 | 262 | public function getYear(): ?string 263 | { 264 | $current_year = Carbon::today()->year; 265 | if (2000 + $this->year < $current_year) { 266 | return '20'.$this->year; 267 | } 268 | 269 | return '19'.$this->year; 270 | } 271 | 272 | public function getMonth(): ?string 273 | { 274 | return $this->month; 275 | } 276 | 277 | public function getDay(): ?string 278 | { 279 | return $this->day; 280 | } 281 | 282 | public function getCodiceFiscale(): ?string 283 | { 284 | return $this->cf; 285 | } 286 | 287 | public function getFirstName(): ?string 288 | { 289 | return substr($this->cf, 3, 3); 290 | } 291 | 292 | public function getLastName(): ?string 293 | { 294 | return substr($this->cf, 0, 3); 295 | } 296 | 297 | /** 298 | * @return array 299 | * @throws CodiceFiscaleValidationException 300 | */ 301 | public function asArray(): array 302 | { 303 | return [ 304 | 'gender' => $this->getGender(), 305 | 'birth_place' => $this->getBirthPlace(), 306 | 'birth_place_complete' => $this->getBirthPlaceComplete(), 307 | 'day' => $this->getDay(), 308 | 'month' => $this->getMonth(), 309 | 'year' => $this->getYear(), 310 | 'birthdate' => $this->getBirthdate(), 311 | 'first_name' => $this->getFirstName(), 312 | 'last_name' => $this->getLastName(), 313 | 'is_international' => $this->isInternational(), 314 | 'is_italian' => $this->isItalian(), 315 | ]; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/CityCodeDecoders/InternationalCitiesStaticList.php: -------------------------------------------------------------------------------- 1 | 'ALBANIA', 9 | 'Z101' => 'ANDORRA', 10 | 'Z102' => 'AUSTRIA', 11 | 'Z103' => 'BELGIO', 12 | 'Z104' => 'BULGARIA', 13 | 'Z105' => 'CECOSLOVACCHIA', 14 | 'Z106' => "CITTA' DEL VATICANO", 15 | 'Z107' => 'DANIMARCA', 16 | 'Z108' => 'ISOLE FAER OER', 17 | 'Z109' => 'FINLANDIA', 18 | 'Z110' => 'FRANCIA', 19 | 'Z111' => 'REPUBBLICA DEMOCRATICA TEDESCA', 20 | 'Z112' => 'GERMANIA', 21 | 'Z113' => 'GIBILTERRA', 22 | 'Z114' => 'REGNO UNITO', 23 | 'Z115' => 'GRECIA', 24 | 'Z116' => 'IRLANDA', 25 | 'Z117' => 'ISLANDA', 26 | 'Z118' => 'REPUBBLICA SOCLIALISTA FEDERALE DI JUGOSLAVIA', 27 | 'Z119' => 'LIECHTENSTEIN', 28 | 'Z120' => 'LUSSEMBURGO', 29 | 'Z121' => 'MALTA', 30 | 'Z122' => 'ISOLA DI MAN', 31 | 'Z123' => 'MONACO', 32 | 'Z124' => 'NORMANNE (ISOLE) O ISOLE DEL CANALE', 33 | 'Z125' => 'NORVEGIA', 34 | 'Z126' => 'PAESI BASSI', 35 | 'Z127' => 'POLONIA', 36 | 'Z128' => 'PORTOGALLO', 37 | 'Z129' => 'ROMANIA', 38 | 'Z130' => 'SAN MARINO', 39 | 'Z131' => 'SPAGNA', 40 | 'Z132' => 'SVEZIA', 41 | 'Z133' => 'SVIZZERA', 42 | 'Z134' => 'UNGHERIA', 43 | 'Z135' => 'UNIONE SOVIETICA', 44 | 'Z138' => 'UCRAINA', 45 | 'Z139' => 'BIELORUSSIA', 46 | 'Z140' => 'MOLDOVA', 47 | 'Z144' => 'ESTONIA', 48 | 'Z145' => 'LETTONIA', 49 | 'Z146' => 'LITUANIA', 50 | 'Z148' => 'MACEDONIA', 51 | 'Z149' => 'CROAZIA', 52 | 'Z150' => 'SLOVENIA', 53 | 'Z153' => 'BOSNIA-ERZEGOVINA', 54 | 'Z154' => 'FEDERAZIONE RUSSA', 55 | 'Z155' => 'SLOVACCHIA', 56 | 'Z156' => 'REPUBBLICA CECA', 57 | 'Z157' => 'SERBIA E MONTENEGRO', 58 | 'Z158' => 'SERBIA', 59 | 'Z159' => 'MONTENEGRO', 60 | 'Z160' => 'KOSOVO', 61 | 'Z161' => "TERRITORI DELL'AUTONOMIA PALESTINESE", 62 | 'Z200' => 'AFGHANISTAN', 63 | 'Z203' => 'ARABIA SAUDITA', 64 | 'Z204' => 'BAHREIN', 65 | 'Z205' => 'BHUTAN', 66 | 'Z206' => 'MYANMAR', 67 | 'Z207' => 'BRUNEI', 68 | 'Z208' => 'CAMBOGIA', 69 | 'Z209' => 'SRI LANKA', 70 | 'Z210' => 'REPUBBLICA POPOLARE CINESE', 71 | 'Z211' => 'CIPRO', 72 | 'Z212' => 'COCOS (ISOLE)', 73 | 'Z213' => 'REPUBBLICA DI COREA', 74 | 'Z214' => 'REPUBBLICA POPOLARE DEMOCRATICA DI COREA', 75 | 'Z215' => 'EMIRATI ARABI UNITI', 76 | 'Z216' => 'FILIPPINE', 77 | 'Z217' => 'TAIWAN', 78 | 'Z218' => 'GAZA (TERRITORIO DI)', 79 | 'Z219' => 'GIAPPONE', 80 | 'Z220' => 'GIORDANIA', 81 | 'Z222' => 'INDIA', 82 | 'Z223' => 'INDONESIA', 83 | 'Z224' => 'IRAN', 84 | 'Z225' => 'IRAQ', 85 | 'Z226' => 'ISRAELE', 86 | 'Z227' => 'KUWAIT', 87 | 'Z228' => 'LAOS', 88 | 'Z229' => 'LIBANO', 89 | 'Z231' => 'MACAO', 90 | 'Z232' => 'MALDIVE', 91 | 'Z233' => 'MONGOLIA', 92 | 'Z234' => 'NEPAL', 93 | 'Z235' => 'OMAN', 94 | 'Z236' => 'PAKISTAN', 95 | 'Z237' => 'QATAR', 96 | 'Z240' => 'SIRIA', 97 | 'Z241' => 'THAILANDIA', 98 | 'Z242' => 'TIMOR ORIENTALE', 99 | 'Z243' => 'TURCHIA', 100 | 'Z246' => 'YEMEN', 101 | 'Z247' => 'MALAYSIA', 102 | 'Z248' => 'SINGAPORE', 103 | 'Z249' => 'BANGLADESH', 104 | 'Z250' => 'YEMEN DEL SUD', 105 | 'Z251' => 'VIETNAM', 106 | 'Z252' => 'ARMENIA', 107 | 'Z253' => 'AZERBAIGIAN', 108 | 'Z254' => 'GEORGIA', 109 | 'Z255' => 'KAZAKHSTAN', 110 | 'Z256' => 'KIRGHIZISTAN', 111 | 'Z257' => 'TAGIKISTAN', 112 | 'Z258' => 'TURKMENISTAN', 113 | 'Z259' => 'UZBEKISTAN', 114 | 'Z300' => 'NAMIBIA', 115 | 'Z301' => 'ALGERIA', 116 | 'Z302' => 'ANGOLA', 117 | 'Z305' => 'BURUNDI', 118 | 'Z306' => 'CAMERUN', 119 | 'Z307' => 'CAPO VERDE', 120 | 'Z308' => 'REPUBBLICA CENTRAFRICANA', 121 | 'Z309' => 'CIAD', 122 | 'Z310' => 'COMORE', 123 | 'Z311' => 'CONGO', 124 | 'Z312' => 'REPUBBLICA DEMOCRATICA DEL CONGO', 125 | 'Z313' => "COSTA D'AVORIO", 126 | 'Z314' => 'BENIN', 127 | 'Z315' => 'ETIOPIA', 128 | 'Z316' => 'GABON', 129 | 'Z317' => 'GAMBIA', 130 | 'Z318' => 'GHANA', 131 | 'Z319' => 'GUINEA', 132 | 'Z320' => 'GUINEA BISSAU', 133 | 'Z321' => 'GUINEA EQUATORIALE', 134 | 'Z322' => 'KENYA', 135 | 'Z324' => 'LA REUNION (ISOLA)', 136 | 'Z325' => 'LIBERIA', 137 | 'Z326' => 'LIBIA', 138 | 'Z327' => 'MADAGASCAR', 139 | 'Z328' => 'MALAWI', 140 | 'Z329' => 'MALI', 141 | 'Z330' => 'MAROCCO', 142 | 'Z331' => 'MAURITANIA', 143 | 'Z332' => 'MAURITIUS', 144 | 'Z333' => 'MOZAMBICO', 145 | 'Z334' => 'NIGER', 146 | 'Z335' => 'NIGERIA', 147 | 'Z336' => 'EGITTO', 148 | 'Z337' => 'ZIMBABWE', 149 | 'Z338' => 'RUANDA', 150 | 'Z340' => "SANT'ELENA", 151 | 'Z341' => "SAO TOME' E PRINCIPE", 152 | 'Z342' => 'SEYCHELLES', 153 | 'Z343' => 'SENEGAL', 154 | 'Z344' => 'SIERRA LEONE', 155 | 'Z345' => 'SOMALIA', 156 | 'Z347' => 'SUD AFRICA', 157 | 'Z348' => 'SUDAN', 158 | 'Z349' => 'SWAZILAND', 159 | 'Z351' => 'TOGO', 160 | 'Z352' => 'TUNISIA', 161 | 'Z353' => 'UGANDA', 162 | 'Z354' => 'BURKINA FASO', 163 | 'Z355' => 'ZAMBIA', 164 | 'Z357' => 'TANZANIA', 165 | 'Z358' => 'BOTSWANA', 166 | 'Z359' => 'LESOTHO', 167 | 'Z360' => 'MAYOTTE', 168 | 'Z361' => 'GIBUTI', 169 | 'Z368' => 'ERITREA', 170 | 'Z400' => 'BERMUDA (ISOLE)', 171 | 'Z401' => 'CANADA', 172 | 'Z402' => 'GROENLANDIA', 173 | 'Z403' => 'SAINT PIERRE E MIQUELON', 174 | 'Z404' => "STATI UNITI D'AMERICA", 175 | 'Z501' => 'ANTILLE OLANDESI', 176 | 'Z502' => 'BAHAMAS', 177 | 'Z503' => 'COSTA RICA', 178 | 'Z504' => 'CUBA', 179 | 'Z505' => 'REPUBBLICA DOMINICANA', 180 | 'Z506' => 'EL SALVADOR', 181 | 'Z507' => 'GIAMAICA', 182 | 'Z508' => 'GUADALUPA', 183 | 'Z509' => 'GUATEMALA', 184 | 'Z510' => 'HAITI', 185 | 'Z511' => 'HONDURAS', 186 | 'Z512' => 'BELIZE', 187 | 'Z513' => 'MARTINICA', 188 | 'Z514' => 'MESSICO', 189 | 'Z515' => 'NICARAGUA', 190 | 'Z516' => 'PANAMA', 191 | 'Z517' => 'PANAMA ZONA DEL CANALE', 192 | 'Z518' => 'PUERTO RICO', 193 | 'Z519' => 'ISOLE TURKS E CAICOS', 194 | 'Z520' => 'VERGINI AMERICANE (ISOLE)', 195 | 'Z522' => 'BARBADOS', 196 | 'Z524' => 'GRENADA', 197 | 'Z525' => 'ISOLE VERGINI BRITANNICHE', 198 | 'Z526' => 'DOMINICA', 199 | 'Z527' => 'SAINT LUCIA', 200 | 'Z528' => 'SAINT VINCENT E GRENADINE', 201 | 'Z529' => 'ANGUILLA (ISOLA)', 202 | 'Z530' => 'ISOLE CAYMAN', 203 | 'Z531' => 'MONTSERRAT', 204 | 'Z532' => 'ANTIGUA E BARBUDA', 205 | 'Z533' => 'SAINT KITTS E NEVIS', 206 | 'Z600' => 'ARGENTINA', 207 | 'Z601' => 'BOLIVIA', 208 | 'Z602' => 'BRASILE', 209 | 'Z603' => 'CILE', 210 | 'Z604' => 'COLOMBIA', 211 | 'Z605' => 'ECUADOR', 212 | 'Z606' => 'GUYANA', 213 | 'Z607' => 'GUYANA FRANCESE', 214 | 'Z608' => 'SURINAME', 215 | 'Z609' => 'MALVINE O FALKLAND (ISOLE)', 216 | 'Z610' => 'PARAGUAY', 217 | 'Z611' => "PERU'", 218 | 'Z612' => 'TRINIDAD E TOBAGO', 219 | 'Z613' => 'URUGUAY', 220 | 'Z614' => 'VENEZUELA', 221 | 'Z700' => 'AUSTRALIA', 222 | 'Z702' => 'CHRISTMAS (ISOLA)', 223 | 'Z703' => 'ISOLE COOK', 224 | 'Z704' => 'FIGI', 225 | 'Z706' => 'GUAM (ISOLA)', 226 | 'Z707' => 'IRIAN OCCIDENTALE', 227 | 'Z708' => 'MACQUARIE (ISOLE)', 228 | 'Z710' => 'MARIANNE (ISOLE)', 229 | 'Z711' => 'ISOLE MARSHALL', 230 | 'Z712' => 'MIDWAY (ISOLE)', 231 | 'Z713' => 'NAURU', 232 | 'Z714' => 'NIUE O SAVAGE (ISOLE)', 233 | 'Z715' => 'NORFOLK (ISOLE E ISOLE DEL MAR DEI CORALLI)', 234 | 'Z716' => 'NUOVA CALEDONIA (ISOLE E DIPENDENZE)', 235 | 'Z719' => 'NUOVA ZELANDA', 236 | 'Z721' => 'ISOLE CILENE (PASQUA E SALA Y GOMEZ)', 237 | 'Z722' => 'PITCAIRN (E DIPENDENZE)', 238 | 'Z723' => 'POLINESIA FRANCESE (ISOLE)', 239 | 'Z724' => 'ISOLE SALOMONE', 240 | 'Z725' => 'SAMOA AMERICANE (ISOLE)', 241 | 'Z726' => 'SAMOA', 242 | 'Z727' => "TOKELAU O ISOLE DELL'UNIONE", 243 | 'Z728' => 'TONGA', 244 | 'Z729' => 'ISOLE WALLIS E FUTUNA', 245 | 'Z730' => 'PAPUA NUOVA GUINEA', 246 | 'Z731' => 'KIRIBATI', 247 | 'Z732' => 'TUVALU', 248 | 'Z733' => 'VANUATU', 249 | 'Z734' => 'PALAU', 250 | 'Z735' => 'STATI FEDERATI DI MICRONESIA', 251 | 'Z800' => 'DIPENDENZE CANADESI', 252 | 'Z801' => 'DIPENDENZE NORVEGESI ARTICHE', 253 | 'Z802' => 'DIPENDENZE RUSSE', 254 | 'Z900' => 'DIPENDENZE AUSTRALIANE', 255 | 'Z901' => 'DIPENDENZE BRITANNICHE', 256 | 'Z902' => 'DIPENDENZE FRANCESI', 257 | 'Z903' => 'DIPENDENZE NEOZELANDESI', 258 | 'Z904' => 'DIPENDENZE NORVEGESI ANTARTICHE', 259 | 'Z905' => 'DIPENDENZE STATUNITENSI', 260 | 'Z906' => 'DIPENDENZE SUDAFRICANE', 261 | 'Z907' => 'SUD SUDAN', 262 | 'Z500' => 'ANTILLE BRITANNICHE', 263 | 'Z521' => 'ANTILLE BRITANNICHE', 264 | 'Z201' => 'ARABIA MERIDIONALE FEDERAZIONE', 265 | 'Z202' => 'ARABIA MERIDIONALE PROTETTORATO', 266 | 'Z137' => 'ARMENIA', 267 | 'Z141' => 'AZERBAIGIAN', 268 | 'Z303' => 'BASUTOLAND-SUD AFRICA BRITANNICO', 269 | 'Z304' => 'BECIUANIA-SUD AFRICA BRITANNICO', 270 | 'Z364' => 'BOPHUTHATSWANA', 271 | 'Z701' => 'CAROLINE (ISOLE)', 272 | 'Z367' => 'CISKEI', 273 | 'Z136' => 'GEORGIA', 274 | 'Z323' => 'IFNI', 275 | 'Z152' => 'KAZAKISTAN', 276 | 'Z142' => 'KIRGHIZISTAN', 277 | 'Z230' => 'MALAYSIA', 278 | 'Z238' => 'RYUKYU (ISOLE)', 279 | 'Z362' => 'SAHARA MERIDIONALE', 280 | 'Z363' => 'SAHARA SETTENTRIONALE', 281 | 'Z339' => 'SAHARA SPAGNOLO', 282 | 'Z239' => 'SIKKIM', 283 | 'Z346' => 'TERRITORIO FRANCESE DEGLI AFAR E DEGLI ISSA', 284 | 'Z147' => 'TAGIKISTAN', 285 | 'Z350' => 'TANGANICA', 286 | 'Z365' => 'TRANSKEI', 287 | 'Z151' => 'TURKMENISTAN', 288 | 'Z143' => 'UZBEKISTAN', 289 | 'Z245' => 'VIETNAM DEL NORD', 290 | 'Z244' => 'VIETNAM DEL SUD', 291 | ]; 292 | 293 | public static function getList() 294 | { 295 | return ItalianCitiesStaticList::getList() + self::list; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Laravel Codice Fiscale](https://banners.beyondco.de/Laravel%20Codice%20Fiscale.png?theme=light&packageManager=composer+require&packageName=robertogallea%2Flaravel-codicefiscale&pattern=charlieBrown&style=style_1&description=Codice+fiscale+validation+and+parsing+is+a+breeze&md=1&showWatermark=0&fontSize=100px&images=identification&widths=200&heights=auto) 2 | 3 | # laravel-codicefiscale 4 | 5 | [![Author][ico-author]][link-author] 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/robertogallea/laravel-codicefiscale.svg?style=flat-square)](https://packagist.org/packages/robertogallea/laravel-codicefiscale) 7 | [![Laravel >=6.0][ico-laravel]][link-laravel] 8 | [![Software License][ico-license]](LICENSE.md) 9 | [![Sponsor me!][ico-sponsor]][link-sponsor] 10 | [![Packagist Downloads][ico-downloads]][link-downloads] 11 | 12 | laravel-codicefiscale is a package for the management of the Italian `CodiceFiscale` (i.e. tax code). 13 | The package allows easy validation and parsing of the CodiceFiscale. It is also suited for Laravel since it provides a 14 | convenient custom validator for request validation. 15 | 16 | ## Laravel Version Compatibility 17 | 18 | | Laravel | Package | 19 | |---------|---------| 20 | | 12.x | ^2.2 | 21 | | 11.x | 2.x | 22 | | 10.x | 1.x | 23 | | 9.x | 1.x | 24 | | 8.x | 1.x | 25 | | 7.x | 1.x | 26 | | 6.x | 1.x | 27 | 28 | > **Important update**: now you can dynamically load city codes from ISTAT using the non-default `IstatRemoteCSVList` 29 | > city decoder. 30 | 31 | - [Installation](#installation) 32 | - [Configuration](#configuration) 33 | - [Validation](#validation) 34 | - [Utility CodiceFiscale class](#utility-codicefiscale-class) 35 | - [Codice fiscale Generation](#codice-fiscale-generation) 36 | - [Faker integration](#faker-integration) 37 | - [City code parsing](#city-code-parsing) 38 | - [Integrate your own cities](#integrate-your-own-cities) 39 | 40 | ## Installation 41 | 42 | Run the following command to install the latest applicable version of the package: 43 | 44 | ```bash 45 | composer require robertogallea/laravel-codicefiscale:^2 46 | ``` 47 | 48 | ### Laravel 49 | 50 | In your app config, add the Service Provider to the `$providers` array *(only for Laravel 5.4 or below)*: 51 | 52 | ```php 53 | 'providers' => [ 54 | ... 55 | robertogallea\LaravelCodiceFiscale\CodiceFiscaleServiceProvider::class, 56 | ], 57 | ``` 58 | 59 | The validation error messages are translated in `it` and `en` languages, if you want to add new language please send me 60 | a PR. 61 | 62 | ### Lumen 63 | 64 | In `bootstrap/app.php`, register the Service Provider 65 | 66 | ```php 67 | $app->register(robertogallea\LaravelCodiceFiscale\CodiceFiscaleServiceProvider::class); 68 | ``` 69 | 70 | ## Configuration 71 | 72 | To customize the package configuration, you must export the configuration file into `config/codicefiscale.php`. 73 | 74 | This can be achieved by launching the following command: 75 | 76 | ``` 77 | php artisan vendor:publish --provider="robertogallea\LaravelCodiceFiscale\CodiceFiscaleServiceProvider" --tag="config" 78 | ``` 79 | 80 | You can configure the following parameters: 81 | 82 | - `city-decoder`: the class used for decoding city codes (see [City code parsing](#city-code-parsing)), default to 83 | `InternationalCitiesStaticList`. 84 | - `date-format`: the date format used for parsing birthdates, default to `'Y-m-d'`. 85 | - `labels`: the labels used for `male` and `female` persons, defaults to `'M'` and `'F'`. 86 | 87 | ## Language Files 88 | 89 | You can customize the validation messages publishing the validation translations with this command: 90 | 91 | ``` 92 | php artisan vendor:publish --provider="robertogallea\LaravelCodiceFiscale\CodiceFiscaleServiceProvider" --tag="lang" 93 | ``` 94 | 95 | ## Validation 96 | 97 | To validate a codice fiscale, use the `codice_fiscale` keyword in your validation rules array 98 | 99 | ```php 100 | public function rules() 101 | { 102 | return [ 103 | 'codicefiscale' => 'codice_fiscale', 104 | 105 | //... 106 | ]; 107 | } 108 | ``` 109 | 110 | From version **1.9.0** you can validate your codice fiscale against other form fields to check whether there is a match 111 | or not. 112 | 113 | You must specify all of the required fields: 114 | 115 | - `first_name` 116 | - `last_name` 117 | - `birthdate` 118 | - `place` 119 | - `gender` 120 | 121 | giving parameters to the `codice_fiscale` rule. 122 | 123 | For example: 124 | 125 | ```php 126 | public function rules() 127 | { 128 | return [ 129 | 'codicefiscale' => 'codice_fiscale:first_name=first_name_field,last_name=last_name_field,birthdate=birthdate_field,place=place_field,gender=gender_field', 130 | 'first_name_field' => 'required|string', 131 | 'last_name_field' => 'required|string', 132 | 'birthdate_field' => 'required|date', 133 | 'place_field' => 'required|string', 134 | 'gender_field' => 'required|string|max:1', 135 | 136 | //... 137 | ]; 138 | } 139 | ``` 140 | 141 | Validation fails if the provided codicefiscale and the one generated from the input fields do not match. 142 | 143 | ## Utility CodiceFiscale class 144 | 145 | A codice fiscale can be wrapped in the `robertogallea\LaravelCodiceFiscale\CodiceFiscale` class to enhance it with 146 | useful utility methods. 147 | 148 | ```php 149 | use robertogallea\LaravelCodiceFiscale\CodiceFiscale; 150 | 151 | ... 152 | try { 153 | $cf = new CodiceFiscale(); 154 | $result = $cf->parse('RSSMRA95E05F205Z'); 155 | var_dump($result); 156 | } catch (Exception $exception) { 157 | echo $exception; 158 | } 159 | ``` 160 | 161 | In case of a valid codicefiscale it produces the following result: 162 | 163 | ```php 164 | [ 165 | "gender" => "M" 166 | "birth_place" => "F205" 167 | "birth_place_complete" => "Milano", 168 | "day" => "05" 169 | "month" => "05" 170 | "year" => "1995" 171 | "birthdate" => Carbon @799632000 { 172 | date: 1995-05-05 00:00:00.0 UTC (+00:00) 173 | } 174 | ] 175 | ``` 176 | 177 | in case of an error, `CodiceFiscale::parse()` throws an `CodiceFiscaleValidationException`, which returns one of the 178 | defined constants with `$exception->getCode()`: 179 | 180 | - `CodiceFiscaleException::NO_ERROR` 181 | - `CodiceFiscaleException::NO_CODE` 182 | - `CodiceFiscaleException::WRONG_SIZE` 183 | - `CodiceFiscaleException::BAD_CHARACTERS` 184 | - `CodiceFiscaleException::BAD_OMOCODIA_CHAR` 185 | - `CodiceFiscaleException::WRONG_CODE` 186 | - `CodiceFiscaleException::MISSING_CITY_CODE` 187 | 188 | If you rather not want to catch exceptions, you can use `CodiceFiscale::tryParse()`: 189 | 190 | ```php 191 | use robertogallea\LaravelCodiceFiscale\CodiceFiscale; 192 | 193 | ... 194 | $cf = new CodiceFiscale(); 195 | $result = $cf->tryParse('RSSMRA95E05F205Z'); 196 | if ($result) { 197 | var_dump($cf->asArray()); 198 | } else { 199 | echo $cf->getError(); 200 | } 201 | ``` 202 | 203 | which returns the same values as above, you can use `$cf->isValid()` to check if the codicefiscale is valid and 204 | `$cf->getError()` to get the error. 205 | This is especially useful in a blade template: 206 | 207 | ```php 208 | @php($cf = new robertogallea\LaravelCodiceFiscale\CodiceFiscale()) 209 | @if($cf->tryParse($codicefiscale)) 210 |

{{$cf->getCodiceFiscale()}}

211 | @else 212 |

{{$cf->getError()->getMessage()}}

213 | @endif 214 | ``` 215 | 216 | ## Codice fiscale Generation 217 | 218 | Class CodiceFiscale could be used to generate codice fiscale strings from input values: 219 | 220 | ```php 221 | $first_name = 'Mario'; 222 | $last_name = 'Rossi'; 223 | $birth_date = '1995-05-05'; // or Carbon::parse('1995-05-05') 224 | $birth_place = 'F205'; // or 'Milano' 225 | $gender = 'M'; 226 | 227 | $cf_string = CodiceFiscale::generate($first_name, $last_name, $birth_date, $birth_place, $gender); 228 | ``` 229 | 230 | ## Faker integration 231 | 232 | You can generate fake codice fiscale in your factories using the provided faker extension: 233 | 234 | ```php 235 | class PersonFactory extends Factory 236 | { 237 | 238 | public function definition(): array 239 | { 240 | return [ 241 | 'first_name' => $firstName = fake()->firstName(), 242 | 'last_name' => $lastName = fake()->lastName(), 243 | 'fiscal_number' => fake()->codiceFiscale(firstName: $firstName, lastName: $lastName), 244 | ]; 245 | } 246 | ``` 247 | 248 | **Note**: you can provide some, all or none of the information required for the generation of codice fiscale 249 | (`firstName`, `lastName`, `birthDate`, `birthPlace`, `gender`) 250 | 251 | ## City code parsing 252 | 253 | There are three strategies for decoding the city code: 254 | 255 | - `InternationalCitiesStaticList`: a static list of Italian cities; 256 | - `ItalianCitiesStaticList`: a static list of International cities; 257 | - `IstatRemoteCSVList`: a dynamic (loaded from web) list of Italian cities loaded from official ISTAT csv file. 258 | Please note that the list is cached (one day by default, see config to change). 259 | - `CompositeCitiesList`: merge the results from two `CityDecoderInterface` classes (for example `IstatRemoteCSVList` and 260 | `InternationalCitiesStaticList`) using the base `CityDecoderInterface` in the config key 261 | `codicefiscale.cities-decoder-list`. 262 | 263 | By default, the package uses the class `InternationalCitiesStaticList` to lookup the city from the code and viceversa. 264 | However, you could use your own class to change the strategy used. 265 | 266 | You just need to implement the `CityDecoderInterface` and its `getList()` method. 267 | Then, to use it, just pass an instance to the `CodiceFiscale` class. 268 | 269 | For example: 270 | 271 | ```php 272 | class MyCityList implements CityDecoderInterface 273 | { 274 | public function getList() 275 | { 276 | // Implementation 277 | } 278 | } 279 | ``` 280 | 281 | ```php 282 | ... 283 | $cf = new CodiceFiscale(new MyCityList) 284 | ... 285 | ``` 286 | 287 | ## Integrate your own cities 288 | 289 | _Note_: if you find missing cities, please make a PR! 290 | 291 | If you want to integrate the cities list, you can use the `CompositeCitiesList` by merging the results of one of the 292 | decoders provided and a custom decoder. 293 | 294 | For example: 295 | 296 | ``` 297 | // conf/codicefiscale.php 298 | 299 | return [ 300 | 'city-decoder' => '\robertogallea\LaravelCodiceFiscale\CityCodeDecoders\CompositeCitiesList', 301 | 302 | ... 303 | 304 | 'cities-decoder-list' => [ 305 | '\robertogallea\LaravelCodiceFiscale\CityCodeDecoders\InternationalCitiesStaticList', 306 | 'YourNamespace\MyCustomList', 307 | ] 308 | ``` 309 | 310 | where `MyCustomList` is defined as: 311 | 312 | ``` 313 | ... 314 | 315 | class MyCustomList implements CityDecoderInterface 316 | { 317 | public function getList() 318 | { 319 | return [ 320 | 'XYZ1' => 'My city 1', 321 | 'XYZ2' => 'My city 2', 322 | ] 323 | } 324 | } 325 | ``` 326 | 327 | [ico-author]: https://img.shields.io/static/v1?label=author&message=robgallea&color=50ABF1&logo=twitter&style=flat-square 328 | 329 | [ico-release]: https://img.shields.io/github/v/release/robertogallea/laravel-codicefiscale 330 | 331 | [ico-downloads]: https://img.shields.io/packagist/dt/robertogallea/laravel-codicefiscale 332 | 333 | [ico-laravel]: https://img.shields.io/static/v1?label=laravel&message=%E2%89%A56.0&color=ff2d20&logo=laravel&style=flat-square 334 | 335 | [ico-sponsor]: https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/robertogallea 336 | 337 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 338 | 339 | [ico-styleci]: https://styleci.io/repos/177130582/shield 340 | 341 | [link-author]: https://twitter.com/robgallea 342 | 343 | [link-release]: https://github.com/robertogallea/laravel-codicefiscale 344 | 345 | [link-downloads]: https://packagist.org/packages/robertogallea/laravel-codicefiscale 346 | 347 | [link-laravel]: https://laravel.com 348 | 349 | [link-sponsor]: https://github.com/sponsors/robertogallea 350 | 351 | [link-styleci]: https://styleci.io/repos/17713058s2/ 352 | --------------------------------------------------------------------------------