medz/gb-t-2260
medz/gb-t-2260 gives PHP and Laravel apps offline access to China's GB/T 2260 administrative division codes, mapping provinces, cities, districts and counties to standard codes. Look up regions by code or name, build cascading address selectors, and seed region tables. Handy for registration forms,...
This is a data-only package, not a Laravel service provider. There is no config to publish, no migrations, no facades — it ships the GB/T 2260 (中华人民共和国行政区划代码) dataset as PHP arrays/objects you can read.
composer require medz/gb-t-2260
First steps:
src/ and read the single class that exposes the dataset (typically GbT2260::provinces(), ::cities($provinceCode), ::areas($cityCode), or a tree() method — verify the exact method names in the source, since the package is not documented beyond the README).GB/T 2260 to understand the 6-digit code layout: PPCCDD where PP = province, CC = city, DD = district/county. 110000 = Beijing, 110100 = Beijing city proper, 110101 = Dongcheng district.<select> elements.use Medz\GbT2260\GbT2260;
$provinces = GbT2260::provinces();
// [['code' => '110000', 'name' => '北京市'], ...]
$cities = GbT2260::cities('440000'); // Guangdong
$areas = GbT2260::areas('440100'); // Guangzhou
If your only need is a static dropdown, this package is enough. If you need province/city/district lookup by code inside a request, wrap it once (see below) rather than calling it repeatedly.
Cascading address selector. Return the three levels as JSON from three thin controller actions or one ?parent=code action. Cache the arrays per level; the underlying data never changes at runtime.
Route::get('/regions/{parent?}', function (?string $parent = null) {
return cache()->rememberForever("regions:$parent", fn () =>
$parent === null
? GbT2260::provinces()
: (str_ends_with($parent, '0000')
? GbT2260::cities($parent)
: GbT2260::areas($parent))
);
});
Denormalized read model. The package data is static and small (~3k rows). Seed your own regions table once via a custom Artisan command, then use Eloquent relationships (Province hasMany City hasMany District). This gives you joins, indexes, and whereIn filtering that the package itself cannot.
// database/seeders/RegionSeeder.php
foreach (GbT2260::provinces() as $p) {
Province::create($p);
foreach (GbT2260::cities($p['code']) as $c) {
City::create($c + ['province_code' => $p['code']]);
foreach (GbT2260::areas($c['code']) as $a) {
District::create($a + ['city_code' => $c['code']]);
}
}
}
Validation rule. Register a custom rule so user-submitted codes cannot drift from the canonical set:
class ValidRegionCode implements ValidationRule {
public function validate(string $attr, mixed $value, Closure $fail): void {
if (! GbT2260::exists((string) $value)) {
$fail('The :attribute is not a valid GB/T 2260 code.');
}
}
}
Address storage. Store the three codes (province_code, city_code, district_code) plus a free-text street field. Never store the names — they change on administrative reshuffles (see below). Resolve names on read via a lookup map built once per request.
Building a tree for a single API response. If your frontend prefers a nested structure, flatten once and memoise:
$tree = cache()->rememberForever('gb2260.tree', function () {
return collect(GbT2260::provinces())->map(fn ($p) => [
...$p,
'children' => collect(GbT2260::cities($p['code']))->map(fn ($c) => [
...$c,
'children' => GbT2260::areas($c['code']),
])->all(),
])->all();
});
Wrapping as a repository. Because the package is a bare static accessor, wrap it in an injectable RegionRepository so the rest of your app never couples to Medz\GbT2260\GbT2260 directly. This is your extension point if you ever swap to a fresher dataset (e.g., scraped from the National Bureau of Statistics) without a project-wide find/replace.
110000 is fine as an int, but county codes like 110101 and town-level codes with leading zeros will bite you if you ever cast to int or store in an INT column. Use CHAR(6) in MySQL and string in PHP.xx0000 maps to a "city" xx0100 labelled 市辖区, and the actual districts hang off that. Your cascading UI must not stop at province for these four — otherwise users in Beijing see no districts.659xxx, 419xxx, 429xxx, 469xxx) break the strict 3-level assumption: a "city" node may contain county-level cities directly. If you build a tree with rigid Province→City→District semantics, you will misclassify these. Handle them as a "no intermediate city" branch, or display the city level as the district level for these parents.710000, 810000, 820000) but typically with no children in this dataset. Handle empty child arrays in your UI (hide the city select instead of showing an empty dropdown).config/app.php providers — there's nothing to boot. Autoloading is enough. If you're on Laravel 11+ and looking for a providers entry, there is none to add.::provinces() calls in a single request are cheap but not free.xxxyyy) codes, historical codes, or pinyin search, this package does not have it — plan to vendor your own dataset and keep the repository abstraction in your app layer.composer.json once seeded — that's the cleanest way to insulate your app from abandonment.How can I help you explore Laravel packages today?