ionepub/idcard
Laravel package for building ID cards from customizable templates. Define card layouts, bind member or user data, and render print-ready cards for students, staff, and event badges. Keeping design in templates so issuing IDs stays simple in your Laravel apps.
ionepub/idcard is a small, single-purpose PHP utility for Chinese second-generation (18-digit) resident ID numbers. It validates the number and extracts birth date, sex, age, and administrative region from the encoded digits. It is not an identity verification service — it only proves the number is structurally and checksum-valid.
Install with Composer:
composer require ionepub/idcard
There is no Laravel service provider or auto-discovery. Instantiate the class directly. The package README is the source of truth for exact method names; the core workflow is a validity check followed by extractor calls.
use ionepub\idcard\Idcard;
$idcard = new Idcard();
$number = strtoupper(trim($request->input('id_number')));
if (! $idcard->check($number)) {
// invalid format, birth date, or checksum
}
$birthday = $idcard->getBirthday($number); // e.g. "1990-01-01"
$sex = $idcard->getSex($number); // e.g. "男" / "女"
$age = $idcard->getAge($number); // integer, based on today
$area = $idcard->getArea($number); // e.g. "北京市朝阳区"
Look first at:
src/ for the main class and its validation logic.getArea() can resolve.composer.json for the PHP version constraint and PSR-4 namespace.The most common first use case is a registration or profile form: validate the ID number, then derive birthday, gender, and region to prefill or cross-check user data.
Because the package is a low-level utility, centralize calls behind a small adapter. This isolates the package’s method names and return types from the rest of the application and gives you one place to normalize input.
namespace App\Services;
use ionepub\idcard\Idcard;
final class IdCardService
{
public function __construct(private Idcard $idcard) {}
public function parse(string $raw): ?array
{
$id = strtoupper(trim($raw));
if (strlen($id) !== 18 || ! $this->idcard->check($id)) {
return null;
}
return [
'number' => $id,
'birthday' => $this->idcard->getBirthday($id),
'sex' => $this->idcard->getSex($id),
'age' => $this->idcard->getAge($id),
'area' => $this->idcard->getArea($id),
];
}
public function isValid(string $raw): bool
{
return $this->parse($raw) !== null;
}
}
Bind it in a service provider if you want injection:
$this->app->singleton(IdCardService::class, function () {
return new IdCardService(new \ionepub\idcard\Idcard());
});
The cleanest integration is a custom validation rule, used in Form Requests or inline rules.
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use ionepub\idcard\Idcard;
class ChineseIdCard implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$id = is_string($value) ? strtoupper(trim($value)) : '';
if ($id === '' || ! (new Idcard())->check($id)) {
$fail('The :attribute must be a valid 18-digit Chinese ID number.');
}
}
}
Form Request with normalization:
protected function prepareForValidation(): void
{
$this->merge([
'id_number' => strtoupper(trim((string) $this->id_number)),
]);
}
public function rules(): array
{
return [
'id_number' => ['required', 'string', 'size:18', new ChineseIdCard()],
];
}
If you validate many IDs in one request, reuse a single Idcard instance rather than constructing one per row.
Use extracted values to populate profile fields, but store the original number securely.
$parsed = app(IdCardService::class)->parse($request->validated('id_number'));
$user->birthday = $parsed['birthday'];
$user->gender = $parsed['sex'] === '男' ? 'male' : 'female';
$user->id_number_encrypted = Crypt::encryptString($parsed['number']);
Compute and display age from the stored birthday at read time. Storing an age integer means it becomes wrong the next day.
$user->age = \Carbon\Carbon::parse($user->birthday)->age;
getArea() is useful for prefilling province/city/district selectors or flagging mismatches between a user-entered address and the ID’s region code. Treat it as a suggestion, not a hard constraint — people move, and region codes change.
$area = $parsed['area']; // e.g. "北京市朝阳区"
$user->address = $user->address ?: $area;
For CSV/Excel imports, validate row by row and collect failures instead of aborting the whole import.
$service = app(IdCardService::class);
$errors = [];
foreach ($rows as $index => $row) {
if (! $service->isValid((string) ($row['id_number'] ?? ''))) {
$errors[] = "Row " . ($index + 2) . ": invalid ID number";
}
}
This is also useful when auditing legacy user tables: find rows whose stored ID number no longer passes the checksum, then decide whether to correct, null, or flag them.
Never return a full ID number in an API response. Mask it in accessors or API resources.
public function getMaskedIdNumberAttribute(): ?string
{
$id = $this->id_number_encrypted
? Crypt::decryptString($this->id_number_encrypted)
: null;
return $id ? substr($id, 0, 6) . '********' . substr($id, -4) : null;
}
Normalize before validating. Chinese ID numbers end in an uppercase X. Users paste lowercase x, add spaces, or submit full-width digits from mobile keyboards. Always strtoupper(trim(...)) and reject full-width characters explicitly.
Do not pass integers. An 18-digit ID number exceeds safe integer handling in many contexts, and leading zeros in region codes will be lost. Accept and store it as a string. This is especially common when importing from Excel, where IDs may arrive in scientific notation or with an apostrophe prefix.
15-digit legacy IDs are a separate format. This package targets the second-generation 18-digit number. If your user base may include older 15-digit IDs, decide explicitly whether to reject them, convert them, or route them to a separate validator. Do not assume check() handles them.
Checksum-valid is not identity-valid. The package verifies format, birth date plausibility, and the weighted checksum. It cannot know whether the number was ever issued, whether it belongs to the person submitting it, or whether it is stolen. Use it as one input validation layer, not as KYC.
Region data is a static snapshot. Area lookups depend on a bundled administrative division table. The package last shipped in August 2020, so newer districts, renames, and mergers may be missing, while obsolete codes may still resolve. If address accuracy is business-critical, maintain your own region table and use the package only for code extraction.
Age is time-dependent and timezone-dependent. getAge() uses the current date. Two servers in different timezones can disagree around midnight, and February 29 birthdays need a policy. Prefer storing the birthday and computing age in application code with an explicit timezone.
Check PHP compatibility before adopting. The last release predates PHP 8.1/8.2/8.3. Run the package’s tests and your own smoke tests on your target PHP version. If the Composer constraint is too tight, do not bypass it with --ignore-platform-req in production; fork or pin deliberately instead.
No framework glue. There is no service provider, facade, config file, or publishable migration. All integration is manual. That is fine for a utility class, but plan for it in your service layer.
Return types may be inconsistent. Extractor methods may return strings, integers, or null/false depending on the input. Cast and null-check at the boundary rather than trusting the type throughout your application. Assert the actual behavior with a few known-good and known-bad fixtures.
Reuse instances in loops. If the class loads a region table on construction, creating one instance per row in a bulk import is wasteful. Construct once and pass it around.
Build a fixture set. Keep a small set of test IDs covering: valid male, valid female, lowercase x, invalid checksum, impossible date (e.g. month 13), invalid region prefix, and empty input. This makes upgrades and PHP version changes safe to verify.
Privacy and compliance matter. Chinese ID numbers are sensitive personal information under PIPL. Encrypt at rest, restrict access, avoid logging the raw value in exceptions or request logs, define retention periods, and mask in UI and API output. Validation failures should not echo the submitted number back to the client.
Do not rely on it for birth-date truth. A checksum-valid number can still encode a birth date that conflicts with the user’s claimed age or documents. Treat extracted data as a cross-check and let users correct profile fields where appropriate.
How can I help you explore Laravel packages today?