laravolt/indonesia
Laravel package for Indonesian regional data: provinces, cities/regencies, districts, and villages, with models, migrations, seeders, and helpers for addresses, postal codes, and location lookups. Quickly add accurate Indonesia area data to your app.
composer require laravolt/indonesia
php artisan vendor:publish --provider="Laravolt\Indonesia\ServiceProvider" # config + migrations
php artisan migrate
php artisan indonesia:seed # ~83k villages, takes minutes
Confirm the exact artisan namespace on your version with php artisan list indonesia — the seed command has been renamed across releases, and publishing is tag-driven (--tag=config, --tag=migrations) if you want to skip one of them.
Where to look first:
config/indonesia.php — the models map. This is the real API surface; everything else (facade, relations, routes) resolves through it.Laravolt\Indonesia\Models\{Province,City,District,Village} — adjacency-list models with province_id / city_id / district_id, plus name and code (BPS code) columns.Laravolt\Indonesia\Indonesia — a query helper (Indonesia::findProvince($id, ['cities']), allProvinces(), etc.) that returns the configured model instances with eager loads.The canonical first use case is a registration/address form: a province <select> populated from Province::orderBy('name')->pluck('name', 'id'), then dependent city → district → village selects. Persist village_id on your address table (it's the finest-grained key and carries postal_code), and derive the rest through the village's parent relations rather than duplicating four columns — unless you need the values to survive a re-seed (see Gotchas).
Cascading selects. Serve one endpoint per level and hit it from Alpine/Livewire/your JS of choice. Filter by parent foreign key, never by name:
Route::get('/cities/{province}', fn (Province $province) =>
$province->cities()->orderBy('name')->get(['id', 'name'])
);
For nested payloads, prefer the package helper over hand-rolled eager loads: Indonesia::findProvince($id, ['cities.districts']).
Model swapping. Don't edit the published models. Subclass them and repoint the config map:
// config/indonesia.php
'models' => ['province' => App\Models\Province::class, /* ... */],
Then add your own relations (addresses(), scopes, accessors like full_name). Anything in the package that resolves models — facade, relations, routes — picks this up. Run php artisan config:cache afterwards, or you'll silently get package models in production while local works.
Validation. Use real integrity checks rather than trusting the client's cascade: Rule::exists('villages', 'id') combined with a Rule::exists('districts', 'id')->where('city_id', $request->city_id) style guard, or a small custom rule that walks the parent chain.
Testing. Never run the full seed in the test suite. Seed a slice in a seeder (Province::insert(...), one city) or import a fixture of ~20 rows; the schema is published so RefreshDatabase gives you the tables for free. If you need realistic data, build factories that call insertOrIgnore with explicit IDs so parent links stay deterministic.
Search / autocomplete. Village names are ambiguous; match on name and show district.name, city.name in the label. Add an index on villages.name (and consider a LIKE 'prefix%' instead of '%term%' — the table is large enough that leading wildcards hurt).
Caching dropdowns. Provinces and cities are effectively static between releases; Cache::rememberForever('id.provinces', fn () => Province::pluck('name','id')) with an explicit invalidation step in your upgrade/seed deploy job removes 95% of the query traffic from address forms.
Data refresh workflow. Treat region data as a deployable artifact: bump the package, publish any new migrations, run the seed command, then run your own reconciliation (e.g. re-point addresses whose village_id disappeared). Wrap the whole thing in a queued job or a deploy hook, not a request.
DB::disableQueryLog(), wrap in a transaction (or chunked transactions), drop indexes before the load and rebuild after, and prefer the packaged seed command's chunking over looping models yourself. On a constrained MySQL instance, importing a plain SQL dump is often 10× faster than the artisan command.TRUNCATE with FKs on will fail; use DELETE or toggle FOREIGN_KEY_CHECKS.code instead of id, every upgrade is a migration. Store village_id locally and treat code as reference data.province_name on an order for historical accuracy, that's fine — but don't also try to keep it in sync with a re-seed. Decide per table.vendor:publish after an upgrade may emit a second migration with a new timestamp; check migrations before running, and add your extra columns (postal_code overrides, custom flags, indexes on province_id) in your own migration rather than editing the package's.config:cache and model swaps. Any config change to models requires a cache clear. A stale cache here presents as "my accessor doesn't exist" errors that look like a bug in the package.Indonesia facade are auto-discovered; on older skeletons you must register both manually. A Class "Indonesia" not found almost always means a missing alias entry, not a broken install./indonesia-ish prefix. Disable it and write your own thin controllers if you already own that namespace or need auth/policies on the endpoints.How can I help you explore Laravel packages today?