ofcold/identity-card
ofcold/identity-card is a Laravel/PHP package for Chinese resident ID numbers. It validates 18-digit identity cards, generates valid numbers for testing, and parses birth date, gender, age, and region details. Use it for form validation, user data checks, and API workflows in Laravel apps or standal...
Install the package:
composer require ofcold/identity-card
Laravel 5.5+ users get auto-discovery; on older versions register Ofcold\IdentityCard\IdentityCardServiceProvider in config/app.php and add 'IdentityCard' => Ofcold\IdentityCard\IdentityCardFacade::class to your aliases.
Publish the config (region data, error messages, default behaviors) before touching any code:
php artisan vendor:publish --provider="Ofcold\IdentityCard\IdentityCardServiceProvider"
Review config/identity-card.php for the region data source and any message overrides.
Look first at:
IdentityCard::make($number) then chained getters).Validator extension point if you plan to wrap it in a FormRequest rule.First use case — validate on entry, parse on success:
use Ofcold\IdentityCard\IdentityCard;
$card = IdentityCard::make($request->input('id_number'));
if ($card === false) {
return back()->withErrors(['id_number' => 'Invalid ID card number.']);
}
return [
'region' => $card->getArea(), // province/city/district string
'birthday' => $card->getBirthday()->toDateString(),
'gender' => $card->getGender(), // 'male' / 'female'
'age' => $card->getAge(),
'zodiac' => $card->getZodiac(),
'constellation'=> $card->getConstellation(),
];
Run a quick Tinker sanity check with a known-good 18-digit number (and one with a deliberately wrong checksum digit) before wiring it into a flow — it's the fastest way to confirm the region data and checksum behave as you expect.
Validation rule integration: Prefer the package's Laravel validation rule over manual calls inside controllers. Use it in a FormRequest:
public function rules(): array
{
return [
'id_number' => ['required', 'string', new IdentityCardRule()],
];
}
This keeps validation declarative and centralised, and surfaces the package's error messages through the standard validation bag.
Service injection vs. facade: For anything beyond a one-liner, inject IdentityCardService (or construct IdentityCard::make() in a small dedicated service) rather than calling the facade from deep inside a controller. It makes the class testable and lets you swap a stub.
Parsing into a DTO: Wrap parsing in your own value object — IdentityData::fromNumber($id) — so controllers and views depend on your shape, not the package's. This is the single most important pattern for avoiding lock-in if you ever swap packages.
Registration / KYC form: Validate format first (fast rule), then resolve region and birthday. Reject future birthdays and implausibly old ones by comparing getBirthday() against now(). Do not trust the derived age alone for compliance checks — recompute from the birthday at the point of use.
Seeding and test fixtures: Use the generator to populate factories with realistic IDs:
User::factory()->create([
'id_number' => IdentityCard::fake()->getIdentityCard(), // or ->generate()
]);
Cache a small pool of generated IDs in setUp() for large seeders so you don't regenerate tens of thousands in a loop.
Bulk imports: When importing CSV of users, validate each row and collect failures rather than throwing on the first invalid ID. A per-row try/catch around IdentityCard::make() — or a truthiness check if false is returned — is safer than letting exceptions halt the import.
Version pinning: Pin to a specific minor ("ofcold/identity-card": "3.3.*") and add a small regression test with a fixed known-good ID plus each derived field (region, birthday, gender, zodiac, constellation). The 3.3.2 release fixed the horoscope/constellation output specifically — that is exactly the kind of output you want locked down by a test.
false vs. exception: Depending on the entry point, IdentityCard::make() may return false on invalid input rather than throwing. Always check truthiness before chaining getters, or you'll get a method-call-on-false in production. If you prefer exceptions, use the explicit validator or try/catch around an exception-throwing method.
15-digit vs. 18-digit: 15-digit legacy IDs have no checksum and no gender digit in the 17th position — the package infers gender from the last digit differently. Some methods (age, checksum) may behave differently or be unavailable for 15-digit numbers. Decide explicitly whether you accept 15-digit IDs at all; for new KYC flows you probably shouldn't. Normalise 15-digit to 18-digit up front if you need a single canonical format.
Checksum is not validity: The checksum (the 18th digit MOD 11-2 rule) catches typos, not fraud. Never equate "passes validation" with "authentic person". Keep a real verification path (carrier/authority API) separate.
Region data staleness: The "region exists" check is only as fresh as the bundled region dataset. Administrative divisions change (new districts, renamed counties). Keep the package up to date, and if you reject valid new-area IDs in production, that's your first suspect — not the checksum.
Birthday returns a date object: getBirthday() typically returns a Carbon/DateTime, not a string. Cast with ->toDateString() before storing, or the Eloquent model will stringify it unexpectedly. Also check the timezone used when computing getAge() — a birthday "today" in UTC may read as tomorrow in Asia/Shanghai.
Zodiac vs. Western zodiac: Chinese "zodiac" (rat, ox, tiger…) and Western "constellation"/"horoscope" (Aries, Leo…) are both exposed. They are different fields — don't confuse getZodiac() with getConstellation(), especially around Chinese New Year (the zodiac changes on lunar new year, not Jan 1) and around cusp dates (e.g. Leo/Virgo boundary). The 3.3.2 patch touched horoscope output — verify cusp boundaries with a fixed test if you surface constellations.
Config caching in production: The published config is cached with php artisan config:cache. If you override messages or change region data paths, run php artisan config:clear && php artisan config:cache or your changes silently won't apply. This is the number one "why is the error message / region check still wrong" debugging step.
Masking for logs and UI: Do not log or render full ID numbers. Mask to 110101********1234 (keep first 6 and last 4) before any Log::info, exception message, or response payload. Store the number encrypted-at-rest or hashed if you only need equality lookups.
Extension points: Prefer a decorator/service around the package for formatting, caching derived profile data, or swapping region sources. Never edit vendor files — composer install will wipe them and the change won't survive a deploy.
Debugging checklist when a real ID won't validate: (1) Trim whitespace and confirm 15/18-digit length; (2) recompute the checksum by hand for the last digit; (3) confirm the region code exists in the current dataset (look it up directly); (4) confirm the birth date is a real calendar date (the package rejects 2023-02-30, but check the year isn't outside the allowed range); (5) check you're not running against a cached config with an old data path.
Testing generators: fake() IDs are structurally valid and are great for factories and seeder data, but do not use them as fixtures for testing specific region/gender assertions unless the generator lets you pin those — otherwise your test is coupled to whatever random value it produced.
How can I help you explore Laravel packages today?