{"id":49900494,"url":"https://github.com/bensondevs/indonesian-ktp","last_synced_at":"2026-05-20T09:01:10.659Z","repository":{"id":357800321,"uuid":"1238557846","full_name":"bensondevs/indonesian-ktp","owner":"bensondevs","description":"Indonesian NIK validation for PHP/Laravel using compiled wilayah data.","archived":false,"fork":false,"pushed_at":"2026-05-18T04:24:00.000Z","size":161,"stargazers_count":28,"open_issues_count":0,"forks_count":11,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-19T08:44:32.263Z","etag":null,"topics":["dukcapil","indonesian","ktp","laravel","nik"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bensondevs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"bensondevs","custom":["https://trakteer.id/bensonsimeon/tip"]}},"created_at":"2026-05-14T08:22:45.000Z","updated_at":"2026-05-19T06:07:12.000Z","dependencies_parsed_at":"2026-05-18T07:01:37.318Z","dependency_job_id":null,"html_url":"https://github.com/bensondevs/indonesian-ktp","commit_stats":null,"previous_names":["bensondevs/indonesian-ktp"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/bensondevs/indonesian-ktp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensondevs%2Findonesian-ktp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensondevs%2Findonesian-ktp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensondevs%2Findonesian-ktp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensondevs%2Findonesian-ktp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bensondevs","download_url":"https://codeload.github.com/bensondevs/indonesian-ktp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bensondevs%2Findonesian-ktp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33252982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-20T04:48:54.280Z","status":"ssl_error","status_checked_at":"2026-05-20T04:48:10.851Z","response_time":356,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dukcapil","indonesian","ktp","laravel","nik"],"created_at":"2026-05-16T05:18:46.014Z","updated_at":"2026-05-20T09:01:10.598Z","avatar_url":"https://github.com/bensondevs.png","language":"PHP","funding_links":["https://github.com/sponsors/bensondevs","https://trakteer.id/bensonsimeon/tip"],"categories":[],"sub_categories":[],"readme":"# 🇮🇩 Indonesian KTP (NIK) validation\n\nValidate Indonesian NIK (*Nomor Induk Kependudukan*) with structural checks and bundled wilayah data (no MySQL, no Nusantara). Public API: [`KTP`](src/KTP.php).\n\n## Table of contents\n\n- [What gets validated](#what-gets-validated)\n- [Requirements](#requirements)\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Usage](#usage)\n  - [Basic validation](#basic-validation)\n  - [Laravel Validator (rule object and ktp-nik)](#laravel-validator-rule-object-and-ktp-nik)\n  - [Quick checks](#quick-checks)\n  - [Expectations and aliases](#expectations-and-aliases)\n  - [validate() and ValidationResult](#validate-and-validationresult)\n  - [Parsed values](#parsed-values)\n  - [Region inputs](#region-inputs)\n  - [Two-digit birth years: ambiguity and asOf()](#two-digit-birth-years-ambiguity-and-asof)\n  - [Region hierarchy lookup](#region-hierarchy-lookup)\n  - [Eloquent HasIndonesianKtp](#eloquent-hasindonesianktp)\n    - [Explicit matchers](#explicit-matchers)\n    - [Trait methods](#trait-methods)\n  - [NIK column and accessors](#nik-column-and-accessors)\n- [Develop and test](#develop-and-test)\n- [Data source](#data-source)\n- [Security](#security)\n- [Versioning and support](#versioning-and-support)\n\n## What gets validated\n\n- **Structure** — length, digits, birth date / gender encoding.\n- **Region hierarchy** — district code in the NIK must exist in [`data/wilayah.php`](data/wilayah.php) (province → regency → subdistrict).\n\nOptional checks (birth, age, gender, wilayah names/codes): [Usage](#usage). Dataset: [Data source](#data-source).\n\nInvalid length or unknown district:\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('123')-\u003eisValid(); // false — wrong length\nKTP::nik('9999991501900001')-\u003eisValid(); // false — unknown district (no expectations)\n```\n\n## Requirements\n\n| Requirement | Notes |\n| --- | --- |\n| PHP | 8.3+ |\n| Laravel | 10–13; `illuminate/contracts`, `illuminate/database`, `illuminate/support`, `illuminate/validation` match your app |\n| Carbon | `nesbot/carbon` ^2.67 or ^3.0 |\n\n## Install 📦\n\n```bash\ncomposer require bensondevs/indonesian-ktp\n```\n\n## Quick start ✅\n\nMinimal check (structure + wilayah hierarchy):\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')-\u003eisValid();\n```\n\nStructure + region only until you add expectations (see [Usage](#usage)).\n\n## Usage\n\n- `KTP::nik($raw)` returns a fluent, **immutable** `Query`: each chained call is a new instance.\n- `isValid()` → one `bool`. `validate()` → [`ValidationResult`](src/NIK/ValidationResult.php) with per-flag detail.\n- Laravel’s [`Validator`](https://laravel.com/docs/validation) is supported via [`KtpNik`](src/Rules/KtpNik.php) and string rules registered in [`IndonesianKtpServiceProvider`](src/IndonesianKtpServiceProvider.php) — see [Laravel Validator (rule object and ktp-nik)](#laravel-validator-rule-object-and-ktp-nik).\n\n```php\nuse Bensondevs\\IndonesianKtp\\Gender;\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')\n    -\u003eexpectGender(Gender::Male)\n    -\u003eisValid();\n```\n\n### Basic validation\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')-\u003eisValid();\n```\n\n### Laravel Validator (rule object and ktp-nik)\n\nWith [package discovery](https://laravel.com/docs/packages#package-discovery), [`IndonesianKtpServiceProvider`](src/IndonesianKtpServiceProvider.php) registers translation lines and validation extensions automatically. You can validate request input either with a [rule object](https://laravel.com/docs/validation#using-rule-objects) or with a string rule.\n\n**Rule object vs string rule**\n\n```php\nuse Bensondevs\\IndonesianKtp\\Rules\\KtpNik;\n\n$request-\u003evalidate([\n    'nik' =\u003e ['required', 'string', new KtpNik],\n]);\n\n// Equivalent string rule (underscore alias: ktp_nik)\n$request-\u003evalidate([\n    'nik' =\u003e ['required', 'string', 'ktp-nik'],\n]);\n```\n\n**What this checks**\n\n| | |\n| --- | --- |\n| Same as | Plain `KTP::nik($value)-\u003eisValid()` — **structure** (length, digits, birth/gender segment rules) plus **complete wilayah hierarchy** for the district code. |\n| Does **not** include | Chained expectations such as `expectBirthDate`, `expectGender`, `expectProvince`, age rules, etc. For those, use the fluent `Query` API — [Expectations and aliases](#expectations-and-aliases). |\n\n**Composing with `required` / `nullable`**\n\nUse Laravel’s built-in rules for presence: `required|string|…` when the field must be present, or `nullable|string|…` when it is optional. The `KtpNik` rule **does not fail** on `null` or `''`, so optional fields stay easy to express without fighting the custom rule.\n\n**Input types**\n\nInteger or numeric string values are cast to string before validation. Arrays, objects, and booleans fail. In practice, pair the rule with Laravel’s `string` rule as in the examples above.\n\n**Messages and localization**\n\nThe default English message lives under the `indonesian-ktp` namespace (`validation.ktp_nik`). Override or translate it like any vendor lang line (for example files under `lang/vendor/indonesian-ktp`). See [Laravel localization](https://laravel.com/docs/localization).\n\n**Custom wilayah data**\n\nIf you rebind [`RegionHierarchyLookup`](src/Regions/Lookup/RegionHierarchyLookup.php), `KTP::nik()` uses it when the container is available — so these validator rules pick up the same lookup. See [Region hierarchy lookup](#region-hierarchy-lookup).\n\n### Quick checks\n\n`match*` helpers compare the NIK to a value; they do **not** add `expect*` rules to the query.\n\n**Gender and birth date**\n\n```php\nuse Bensondevs\\IndonesianKtp\\Gender;\nuse Bensondevs\\IndonesianKtp\\KTP;\n\n$query = KTP::nik('3315131501901235');\n\n$query-\u003ematchBirthDate('1990-01-15');\n$query-\u003ematchGender(Gender::Male);\n$query-\u003ematchGender('male');\n```\n\n**Age / minimum age:** `matchAge()` / `matchAtLeastYears()` need a resolved birth year — use `asOf()` as in [Two-digit birth years](#two-digit-birth-years-ambiguity-and-asof).\n\n### Expectations and aliases\n\nChain then call `isValid()` or `validate()`. Each chained call returns a new `Query`.\n\n| Area | `expect*` | Alias |\n| --- | --- | --- |\n| Birth date | `expectBirthDate` | `birthDate` |\n| Integer age | `expectAge` | `age` |\n| Minimum age | `expectAtLeastYears`, `expectSeventeenOrOlder`, `expectTwentyOneOrOlder` | — |\n| Gender | `expectGender` | `gender` |\n| Province | `expectProvince` | `province` |\n| Regency | `expectRegency` | `regency` |\n| Subdistrict | `expectSubdistrict` | `subdistrict` |\n\n**Full names**\n\n```php\nuse Bensondevs\\IndonesianKtp\\Gender;\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')\n    -\u003eexpectBirthDate('1990-01-15')\n    -\u003eexpectGender(Gender::Male)\n    -\u003eexpectProvince('jawa tengah')\n    -\u003eisValid();\n```\n\n**Aliases**\n\n```php\nuse Bensondevs\\IndonesianKtp\\Gender;\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')\n    -\u003ebirthDate('1990-01-15')\n    -\u003egender(Gender::Male)\n    -\u003eprovince('jawa tengah')\n    -\u003eisValid();\n```\n\n`isValid()` is the same as `validate()-\u003eisFullyValid()` (structure + hierarchy + every set expectation). Chaining `expectAge` / `age` needs `asOf()` — see [Two-digit birth years](#two-digit-birth-years-ambiguity-and-asof).\n\n### validate() and ValidationResult\n\n[`ValidationResult`](src/NIK/ValidationResult.php) exposes **methods** (not public properties).\n\n| Method | Return | Notes |\n| --- | --- | --- |\n| `hasValidStructure()` | `bool` | |\n| `hasValidRegionHierarchy()` | `bool` | |\n| `hasValidBirthDate()` | `bool` or `null` | `null` = expectation not set |\n| `hasValidGender()` | `bool` or `null` | |\n| `hasValidProvince()` | `bool` or `null` | |\n| `hasValidRegency()` | `bool` or `null` | |\n| `hasValidSubdistrict()` | `bool` or `null` | |\n| `hasValidAge()` | `bool` or `null` | |\n| `hasValidMinimumAge()` | `bool` or `null` | |\n| `isFullyValid()` | `bool` | Same as `isValid()` on the `Query` |\n\n| Alias | Equivalent |\n| --- | --- |\n| `hasValidKabupaten()`, `hasValidCity()` | `hasValidRegency()` |\n| `hasValidKecamatan()` | `hasValidSubdistrict()` |\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\n$validationResult = KTP::nik('3315131501901235')-\u003evalidate();\n\n$validationResult-\u003ehasValidStructure();\n$validationResult-\u003ehasValidRegionHierarchy();\n$validationResult-\u003ehasValidGender(); // null — no expectation\n\n$validationResult = KTP::nik('3315131501901235')\n    -\u003ebirthDate('1990-01-01')\n    -\u003evalidate();\n\n$validationResult-\u003ehasValidBirthDate(); // false — mismatch\n```\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\n$validationResult = KTP::nik('3315131501901235')-\u003evalidate();\n\n$validationResult-\u003eisFullyValid(); // same as isValid() on the query\n```\n\n### Parsed values\n\n`parsed()` returns a [`Parsed`](src/NIK/Parsed.php) snapshot (read-only fields from the NIK). `KTP::nik(...)-\u003eparsed()` attaches wilayah **names** from the app’s region lookup when the district code is known; use `provinceCode()` / `regencyCode()` / `districtCode()` for keys from the NIK alone.\n\n| Method | Role |\n| --- | --- |\n| `raw()` | Normalized 16-digit string |\n| `structureValid()` | Structural segment checks |\n| `provinceCode()`, `regencyCode()`, `districtCode()` | Wilayah **codes** from the NIK (e.g. `33`, `33.15`, `33.15.13`) |\n| `province()`, `provinsi()` | Province **display name** when the bound lookup resolves the district (e.g. `Jawa Tengah`); `null` if unknown or parser-only `Parsed` without `withRegionHierarchy()` |\n| `regency()`, `kabupaten()`, `kota()`, `city()` | Regency / city **display name** when resolved (same value for all four; NIK does not distinguish kabupaten vs kota); `null` otherwise |\n| `district()`, `kecamatan()` | Kecamatan **display name** when resolved; `null` otherwise |\n| `birthDate()` | Single date, or `null` if two-digit year is ambiguous ([Two-digit birth years](#two-digit-birth-years-ambiguity-and-asof)) |\n| `possibleBirthDates()` | All plausible dates when ambiguous |\n| `gender()`, `serial()` | Parsed gender / serial |\n| `age($asOf?)`, `isAtLeastYears($min, $asOf)`, `isSeventeenOrOlder($asOf)`, `isTwentyOneOrOlder($asOf)` | Age helpers (conservative when ambiguous). `age()` with no argument uses the current instant (`Carbon::now()`). |\n\nOn the `Query`, `resolvedAge()` uses the pivot instant when you chained `asOf()` ([Two-digit birth years](#two-digit-birth-years-ambiguity-and-asof)). For real validation, prefer `validate()` / `isValid()`.\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\nuse Carbon\\Carbon;\n\n$parsed = KTP::nik('3315131501901235')-\u003eparsed();\n\n$parsed-\u003eraw();\n$parsed-\u003estructureValid();\n$parsed-\u003edistrictCode();              // NIK wilayah key, e.g. \"33.15.13\"\n$parsed-\u003eprovinceCode();              // \"33\"\n$parsed-\u003eregencyCode();               // \"33.15\"\n$parsed-\u003eprovince();                 // e.g. \"Jawa Tengah\" — null if lookup has no row\n$parsed-\u003eregency();                  // e.g. \"Kabupaten Grobogan\"; alias: city(), kabupaten(), kota()\n$parsed-\u003edistrict();                 // e.g. \"Purwodadi\"; alias: kecamatan()\n$parsed-\u003ebirthDate();                // null if ambiguous (no asOf on query)\n$parsed-\u003epossibleBirthDates();\n$parsed-\u003egender();\n$parsed-\u003eserial();\n$parsed-\u003eage();                      // optional asOf; defaults to now()\n$parsed-\u003eage(Carbon::parse('2026-01-01'));\n$parsed-\u003eisSeventeenOrOlder(Carbon::parse('2026-01-01'));\n```\n\n### Region inputs\n\n[`NikRegionMatcher`](src/Regions/Matching/NikRegionMatcher.php): province, regency, and subdistrict each accept **codes** (int / string shapes) or **names**. More cases: [`tests/Feature/Ktp/KtpRegionInputsTest.php`](tests/Feature/Ktp/KtpRegionInputsTest.php).\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\n$sampleNik = '3315131501901235';\n\nKTP::nik($sampleNik)-\u003eexpectProvince(33)-\u003eisValid();\nKTP::nik($sampleNik)-\u003eexpectProvince('JAWA TENGAH')-\u003eisValid();\n```\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')\n    -\u003eexpectRegency(15) // province taken from NIK (33…)\n    -\u003eisValid();\n```\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('3315131501901235')\n    -\u003eexpectRegency(15)\n    -\u003eexpectSubdistrict(13)\n    -\u003eisValid();\n```\n\nUnknown district:\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\n\nKTP::nik('9999991501900001')-\u003eisValid(); // false\n```\n\n### Two-digit birth years: ambiguity and asOf()\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\nuse Carbon\\Carbon;\n\n// No asOf: every plausible century for YY that fits a 17–120 age window (evaluated at query build time)\nKTP::nik('3315131501901235');\n\n// With asOf: single resolved birth year for that pivot\nKTP::nik('3315131501901235')-\u003easOf(Carbon::parse('2026-01-01'));\n```\n\n**`matchAge` / minimum age** (needs the same pivot):\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\nuse Carbon\\Carbon;\n\n$query = KTP::nik('3315131501901235')-\u003easOf(Carbon::parse('2026-01-01'));\n\n$query-\u003ematchAge(35);\n$query-\u003ematchAtLeastYears(21);\n```\n\n| Topic | Behaviour |\n| --- | --- |\n| Birth / `expectBirthDate` | Ambiguous: any matching candidate wins. Pivot: one resolved year. |\n| `parsed()-\u003ebirthDate()` | `null` if multiple candidates; use `possibleBirthDates()`. Pivot: always set when structure is valid. |\n| `age` / `resolvedAge()` | Can stay `null` until year is unique; use `asOf()` or derive from `possibleBirthDates()`. |\n| Minimum-age helpers | **Conservative:** every plausible birth candidate must pass. |\n\n**Edge cases:** 17–120 uses calendar boundaries; odd ages or midnight tests may need your own `asOf()`. Examples: [`tests/Feature/Ktp/KtpTwoDigitYearAndAsOfTest.php`](tests/Feature/Ktp/KtpTwoDigitYearAndAsOfTest.php), [`tests/Unit/NIK/ParserTest.php`](tests/Unit/NIK/ParserTest.php).\n\n```php\nuse Bensondevs\\IndonesianKtp\\KTP;\nuse Carbon\\Carbon;\n\nCarbon::setTestNow('2026-09-01');\n\n$ambiguousNik = '3315130109090002';\n\nKTP::nik($ambiguousNik)-\u003eparsed()-\u003ebirthDate();              // null\ncount(KTP::nik($ambiguousNik)-\u003eparsed()-\u003epossibleBirthDates()); // 2\n\nKTP::nik($ambiguousNik)-\u003easOf(Carbon::parse('2026-09-01'))-\u003eparsed()-\u003ebirthDate(); // single date\n\nCarbon::setTestNow();\n```\n\nEloquent: same behaviour via `indonesianKtpReferenceDate()` — [Reference date](#reference-date-indonesianktpreferencedate) under [NIK column and accessors](#nik-column-and-accessors).\n\n### Region hierarchy lookup\n\n- **Auto-discovery:** [`IndonesianKtpServiceProvider`](src/IndonesianKtpServiceProvider.php) registers [`RegionHierarchyLookup`](src/Regions/Lookup/RegionHierarchyLookup.php) → bundled [`data/wilayah.php`](data/wilayah.php) via [`FileRegionHierarchyLookup`](src/Regions/Lookup/FileRegionHierarchyLookup.php).\n- **[`KTP::nik()`](src/KTP.php)** uses the container when the contract is bound; otherwise the bundled path (e.g. some unit tests).\n\nCustom compiled file (same PHP array format), rebind **after** the package provider:\n\n```php\nuse Bensondevs\\IndonesianKtp\\Regions\\Lookup\\FileRegionHierarchyLookup;\nuse Bensondevs\\IndonesianKtp\\Regions\\Lookup\\RegionHierarchyLookup;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass AppServiceProvider extends ServiceProvider\n{\n    public function register(): void\n    {\n        $this-\u003eapp-\u003esingleton(RegionHierarchyLookup::class, function (): FileRegionHierarchyLookup {\n            $path = storage_path('app/wilayah.php'); // your compiled file\n\n            return new FileRegionHierarchyLookup($path);\n        });\n    }\n}\n```\n\n### Eloquent HasIndonesianKtp\n\n[`HasIndonesianKtp`](src/Concerns/HasIndonesianKtp.php) reads the **NIK column** via `getAttribute()` (casts and accessors apply). Comparisons against birth date, age, gender, and wilayah fields are **explicit**: you pass the value you want checked (for example from another column, a relation, or request input). The trait does not scan `nik_*` or fallback attribute names for you.\n\n```php\nuse Bensondevs\\IndonesianKtp\\Concerns\\HasIndonesianKtp;\nuse Illuminate\\Foundation\\Auth\\User as Authenticatable;\n\nclass User extends Authenticatable\n{\n    use HasIndonesianKtp;\n\n    // Optional: if your NIK column is not named `nik`\n    // protected function getIndonesianKtpNikColumn(): string\n    // {\n    //     return 'id_number';\n    // }\n}\n```\n\n```php\n// $model is an Eloquent model using HasIndonesianKtp\n\n$model-\u003ehasValidNik();\n$model-\u003enikGenderIs($model-\u003egender);\n$model-\u003enikProvinceIs($model-\u003eprovince);\n$model-\u003enikAgeIs((int) $model-\u003eage);\n```\n\nDefault NIK attribute name: `nik` (override `getIndonesianKtpNikColumn()`). Non-digits are stripped after read.\n\n#### Explicit matchers\n\nEach `nik*Is($value)` method compares the **argument** to what is encoded or implied by the NIK. Extra attributes on the model are ignored unless you pass them in. Each short name has a long alias (`indonesianIdNumber*Is`) for consistency with the rest of the package.\n\n#### Trait methods\n\n| Method | Purpose |\n| --- | --- |\n| `hasValidNik()` | Structure + district hierarchy; same as `hasValidIndonesianIdNumber()` |\n| `nikBirthdateIs(mixed $birth)` | NIK birth segment matches `$birth`; alias `indonesianIdNumberBirthdateIs()` |\n| `nikGenderIs()` | NIK gender matches the argument (`Gender` or `string`); alias `indonesianIdNumberGenderIs()` |\n| `nikProvinceIs(mixed $expected)` | Wilayah province expectation; alias `indonesianIdNumberProvinceIs()` |\n| `nikRegencyIs(mixed $expected)` | Regency expectation; aliases `indonesianIdNumberRegencyIs()`, `nikKabupatenIs()`, `nikCityIs()`, `nikDistrictIs()` and matching `indonesianIdNumber*` forms |\n| `nikSubdistrictIs(mixed $expected)` | Subdistrict expectation; aliases `indonesianIdNumberSubdistrictIs()`, `nikKecamatanIs()`, `indonesianIdNumberKecamatanIs()` |\n| `nikAgeIs(int $age)` | Completed age from the NIK (per reference date rules) equals `$age` when unambiguous; alias `indonesianIdNumberAgeIs()` |\n| `ageFromNik()` | Completed full years from the NIK at the trait’s reference instant; `null` when ambiguous; alias `ageFromIndonesianIdNumber()` |\n| `isSeventeenOrOlderFromNik()` | Conservative 17+ check over all birth candidates; alias `isSeventeenOrOlderFromIndonesianIdNumber()` |\n| `isTwentyOneOrOlderFromNik()` | Conservative 21+ check; alias `isTwentyOneOrOlderFromIndonesianIdNumber()` |\n| `isAtLeastYearsFromNik(int $years)` | Conservative minimum-age check; alias `isAtLeastYearsFromIndonesianIdNumber()` |\n\n### NIK column and accessors\n\nOverride `getIndonesianKtpNikColumn()` and/or use an accessor on that column. The trait does not expose raw NIK normalization beyond stripping non-digits after `getAttribute`.\n\n#### NIK column name\n\n```php\nprotected function getIndonesianKtpNikColumn(): string\n{\n    return 'national_id';\n}\n```\n\n#### NIK value (accessor on the configured column)\n\nFormatted storage → normalize in an accessor; the trait still strips non-digits after `getAttribute`.\n\n```php\n// With default getIndonesianKtpNikColumn() =\u003e 'nik'\nprotected function getNikAttribute(?string $value): ?string\n{\n    return $value !== null ? preg_replace('/\\D/', '', $value) : null;\n}\n```\n\n#### Applicant-style: custom column names, explicit matchers\n\nWhen birth date, gender, or wilayah live on other attributes or relations, read them yourself and pass them into `nik*Is`:\n\n```php\nuse Bensondevs\\IndonesianKtp\\Concerns\\HasIndonesianKtp;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Applicant extends Model\n{\n    use HasIndonesianKtp;\n\n    protected function getIndonesianKtpNikColumn(): string\n    {\n        return 'identity_number';\n    }\n}\n\n// Validation-style usage (attributes / casts apply on reads):\n$applicant-\u003ehasValidNik()\n    \u0026\u0026 $applicant-\u003enikBirthdateIs($applicant-\u003eprofile?-\u003edob ?? $applicant-\u003elegacy_birthdate)\n    \u0026\u0026 $applicant-\u003enikGenderIs($applicant-\u003esex_code)\n    \u0026\u0026 $applicant-\u003enikProvinceIs($applicant-\u003eprov_name)\n    \u0026\u0026 $applicant-\u003enikRegencyIs($applicant-\u003ekab_name)\n    \u0026\u0026 $applicant-\u003enikSubdistrictIs($applicant-\u003ekec_name);\n```\n\n#### Reference date (indonesianKtpReferenceDate)\n\nDefault `null` → ambiguous two-digit years (like `KTP::nik($nik)` without `asOf()`). Return `Carbon::now()` (or any pivot) so every internal trait query uses `asOf()` for YY resolution.\n\n```php\nuse Carbon\\Carbon;\nuse Carbon\\CarbonInterface;\n\nprotected function indonesianKtpReferenceDate(): ?CarbonInterface\n{\n    return null; // ambiguous\n}\n\nprotected function indonesianKtpReferenceDate(): ?CarbonInterface\n{\n    return Carbon::now(); // pivot on “now”\n}\n```\n\n## Develop and test 🧪\n\n```bash\ncomposer install \u0026\u0026 composer test\n```\n\n## Data source\n\nHierarchy file: [`data/wilayah.php`](data/wilayah.php) (from [cahyadsn/wilayah](https://github.com/cahyadsn/wilayah), MIT). Attribution: [`NOTICE`](NOTICE). Maintainers can compile from [upstream `db/wilayah.sql`](https://github.com/cahyadsn/wilayah) and ship their own `wilayah.php`; this repo has no compile script.\n\n## Security 🔒\n\nValidation does **not** send NIKs off-device. Treat NIKs as sensitive in logs and traces. Disclosure: [`SECURITY.md`](SECURITY.md).\n\n## Versioning and support\n\n[Semantic Versioning](https://semver.org/). Upgrades: [`CHANGELOG.md`](CHANGELOG.md).\n\n- **Releases:** [Packagist — bensondevs/indonesian-ktp](https://packagist.org/packages/bensondevs/indonesian-ktp)\n- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)\n- **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)\n- **Security:** [`SECURITY.md`](SECURITY.md)\n\nMatch supported Laravel majors to [`composer.json`](composer.json) `illuminate/*` constraints when upgrading.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbensondevs%2Findonesian-ktp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbensondevs%2Findonesian-ktp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbensondevs%2Findonesian-ktp/lists"}