Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Hanhchinhvn Laravel Package

vanthao03596/laravel-hanhchinhvn

Laravel package for Vietnam administrative divisions: import and query provinces, districts, and wards/communes with Eloquent models, migrations, and data seeding for building address forms and location-based features.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require vanthao03596/laravel-hanhchinhvn
php artisan vendor:publish --provider="Vanthao03596\LaravelHanhchinhVn\HanhchinhVnServiceProvider"
php artisan migrate

The package ships three things you care about: migrations for the administrative tables (provinces → districts → wards), Eloquent models for each level, and a data set to seed. On a fresh install the fastest path to a working state is: publish, migrate, then run the package's seed step (either the seeder the README calls out, or php artisan db:seed --class="Vanthao03596\LaravelHanhchinhVn\Database\Seeders\...Seeder" if no artisan command is provided). Confirm the exact class/command name against the README — it changed shape across the author's other packages.

Where to look first

  1. config/hanhchinhvn.php (after publishing) — table names, model bindings, any connection override.
  2. src/Models/ — you need the exact class names (Province, District, Ward, or the Vietnamese equivalents) and the relationship method names before writing any controller code.
  3. database/migrations/ — decide early whether the keys are auto-increment ids or the official GSO code. This decision ripples through every place you store an address.

First use case

A cascading address picker on a checkout or registration form. Render the province list server-side, load districts on change, wards on district change. Store the ward (most granular) and derive district/province by walking up the relationships — never store three separate free-text fields.

Implementation Patterns

Cascading selects via JSON endpoints

Route::get('/api/districts/{province}', fn (Province $province) =>
    $province->districts()->select('id', 'name')->orderBy('name')->get()
);

Keep the payloads tiny (select('id','name')), eager-load on the parent, and cache the top-level province list — it changes almost never:

$provinces = Cache::rememberForever('provinces', fn () =>
    Province::orderBy('name')->get(['id', 'name'])
);

Front-end: Select2/AJAX for district/ward, wire:model + updated hooks in Livewire, or a plain fetch chain. The pattern is identical regardless of front-end.

Storing addresses

Persist the leaf key (ward_id or ward_code) plus the street line. Derive the rest:

$ward = Ward::with('district.province')->findOrFail($request->ward_id);
$address = $order->address()->create([
    'street'     => $request->street,
    'ward_id'    => $ward->id,
    'district_id'=> $ward->district_id,
    'province_id'=> $ward->district->province_id,
]);

Denormalising the parent IDs avoids joins on the hot read path and keeps historical orders correct even if the reference tables are later re-seeded.

Validation

'ward_id' => ['required', Rule::exists('wards', 'id')],

Cross-field consistency (ward actually belongs to the selected district) is not enforced by exists — either trust the front end or add a Rule::in against $district->wards()->pluck('id').

Search by name

Vietnamese names are the hardest part of this package. For an autocomplete:

Ward::query()
    ->where('name', 'like', "%{$q}%")
    ->with('district.province:id,name')
    ->limit(20)
    ->get();

MySQL's utf8mb4_unicode_ci handles case folding but not diacritic-insensitivity (Đ ≠ D, ả ≠ a). If users type without accents, add a normalised name_ascii column populated at seed time and index it — otherwise your like-query silently returns nothing.

Testing

Do not run the full seed in RefreshDatabase. Insert a handful of provinces/districts/wards in a fixture or factory, or wrap tests in DatabaseTransactions against a pre-seeded local DB. The full ward table is ~11k rows; re-seeding per test will dominate your suite runtime.

Livewire / Inertia

Both are straightforward. In Livewire, clear the dependent properties in updatedProvinceId() before reloading; in Inertia, return the child list as a prop from a partial reload (only: ['districts']) so the cascading feels instant without a page navigation.

Gotchas and Tips

The data is from 2022 — and Vietnam reorganised in 2025. This is the single most important caveat. The package froze at its March 2022 release. It contains the old three-tier structure (province → district → ward). Vietnam abolished the district tier effective mid-2025 and merged provinces/wards, so the reference data is materially out of date for current production use. Plan to either fork the package and refresh the seed from the official source (danhmuchanhchinh.gso.gov.vn) or treat the tables as a starting point you maintain yourself. Do not assume "released" means "current".

IDs vs codes. If the schema uses auto-increment IDs, any truncate-and-reseed changes them — silently breaking every saved address. If it exposes a stable code/gso_id, prefer that as the foreign key in your tables. Check the migrations before designing your address model; retrofitting this later means a data migration.

Table-name collisions. The migrations create provinces, districts, wards. If you already have any of those (common in e-commerce codebases), publishing the migrations will fail or, worse, attach your data to the wrong schema. Rename via the published config if supported, otherwise edit the migration before running it.

Seeding is slow. A few thousand districts and wards inserted one-by-one through Eloquent will take minutes. If you need to re-seed repeatedly: wrap in a transaction, DB::disableQueryLog(), chunk inserts, and disable FK checks (Schema::disableForeignKeyConstraints()).

Stale cache after reseed. rememberForever on provinces is great until you update the data and forget to bust it. Add a versioned key (provinces:v2) or flush on deploy; in local dev this bites constantly.

Laravel version drift. The 2022 release targets Laravel 8/9. On Laravel 10/11 you may need --ignore-platform-reqs or a fork with the constraint widened. Verify the service provider's provide()/boot() still work before committing to the dependency.

Extension points. The clean places to customise are: published config (table/model names), a subclassed model if you need a full_name accessor ("Xã A, Huyện B, Tỉnh C"), and a replacement seeder that reads your own JSON. Avoid editing vendor/ — this package has ~39 stars and no active maintenance, so you will end up forking; keep your changes in app/ so the fork stays trivial.

Route model binding on codes. Route::get('/api/districts/{province}') binds on the primary key by default. If you expose codes in URLs, add getRouteKeyName() => 'code' on the model or use explicit where.

Community support is thin. MIT-licensed, single maintainer, last release 2022. Budget for reading the source (it's small) rather than expecting issues to be answered. Vendor the data set or pin the version in composer.lock and document the fork path in your team's onboarding notes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log