aliziodev / laravel-wilayah
Laravel package untuk data wilayah administratif Indonesia (Provinsi, Kabupaten/Kota, Kecamatan, Desa/Kelurahan) yang selalu up-to-date.
Requires
- php: ^8.2|^8.3
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-drift: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
- aliziodev/laravel-wilayah-boundaries: Tambahkan data polygon batas wilayah untuk fitur peta/GIS
- aliziodev/laravel-wilayah-logos: Tambahkan asset logo/lambang daerah (Provinsi dan Kab/Kota)
README
Data wilayah administratif Indonesia (Provinsi → Kabupaten/Kota → Kecamatan → Desa/Kelurahan) untuk Laravel — selalu up-to-date via CI/CD otomatis dari upstream
cahyadsn/wilayahdancahyadsn/wilayah_kodepos.
📋 Daftar Isi
- Fitur
- Persyaratan
- Instalasi
- Konfigurasi
- Penggunaan
- Fitur Opsional
- Artisan Commands
- Update Data
- Package Tambahan
- Testing
Fitur
| Fitur | Keterangan |
|---|---|
| 🗺 4 Level Wilayah | Provinsi, Kabupaten/Kota, Kecamatan, Desa/Kelurahan |
| 📮 Kode Pos | Terintegrasi dari cahyadsn/wilayah_kodepos |
| ⚡ Cache Otomatis | TTL per tipe query, driver configurable |
| 🔍 Pencarian | LIKE search, Full-text (MySQL/PostgreSQL), alamat lengkap, kode pos |
| 🌳 Hierarki | toAddress(), toShortAddress(), ancestors() |
| 📋 Dropdown/Select | Format cascade select & Livewire-ready |
| 📄 Paginasi | paginate(), simplePaginate(), cursorPaginate() |
| 🏝 Data Opsional | Islands, Luas Wilayah, Populasi (toggle via config) |
| 🔄 Auto-Sync CI/CD | Data dan metadata rilis diperbarui otomatis saat upstream update |
| 🐘 MySQL & PostgreSQL | Dukungan penuh kedua database |
Persyaratan
- PHP ^8.2
- Laravel 11.x / 12.x / 13.x
- MySQL 8.0+ atau PostgreSQL 15+
Instalasi
composer require aliziodev/laravel-wilayah
Instalasi Cepat (Otomatis)
php artisan wilayah:install
Perintah ini akan otomatis: publish config, publish migrasi, jalankan migrate, dan seed data.
Instalasi Manual
php artisan vendor:publish --tag=wilayah-config php artisan vendor:publish --tag=wilayah-migrations php artisan migrate php artisan wilayah:seed
Konfigurasi
File konfigurasi ada di config/wilayah.php:
return [ // Model (bisa di-override dengan model kustom) 'models' => [ 'province' => \Aliziodev\Wilayah\Models\Province::class, 'regency' => \Aliziodev\Wilayah\Models\Regency::class, 'district' => \Aliziodev\Wilayah\Models\District::class, 'village' => \Aliziodev\Wilayah\Models\Village::class, ], // Fitur opsional (Island, Area, Population) 'features' => [ 'islands' => false, // Aktifkan data pulau 'areas' => false, // Aktifkan data luas wilayah 'populations' => false, // Aktifkan data penduduk ], // Cache 'cache' => [ 'enabled' => true, 'driver' => env('WILAYAH_CACHE_DRIVER', 'default'), 'ttl' => [ 'provinces' => 86400, // 24 jam 'regencies' => 86400, 'districts' => 3600, // 1 jam 'villages' => 3600, ], ], // Tabel (bisa di-override) 'tables' => [ 'provinces' => 'provinces', 'regencies' => 'regencies', 'districts' => 'districts', 'villages' => 'villages', ], ];
Penggunaan
Semua fitur tersedia via Facade:
use Aliziodev\Wilayah\Facades\Wilayah;
Pencarian Wilayah
// Ambil semua — mengembalikan Query Builder Wilayah::provinces()->get(); Wilayah::regencies()->get(); Wilayah::districts()->get(); Wilayah::villages()->get(); // Filter berdasarkan induk Wilayah::regencies('32')->get(); // Kab/Kota di Jawa Barat Wilayah::districts('32.73')->get(); // Kecamatan di Kota Bandung Wilayah::villages('32.73.07')->get(); // Desa di Kec. Cicendo // Cari berdasarkan nama (semua level sekaligus) $result = Wilayah::search('Bandung'); // Returns: ['provinces' => [...], 'regencies' => [...], 'districts' => [...], 'villages' => [...]] // Cari spesifik level Wilayah::provinces()->where('name', 'like', '%barat%')->get(); // Cari dengan prefix kode Wilayah::findByCodePrefix('32.73')->get(); // Semua kecamatan di Kota Bandung // Full-text search (butuh FULLTEXT/tsvector index di database) Wilayah::fullTextSearch('Sunter')->paginate(15); // Pencarian alamat lengkap (multiple keyword) $result = Wilayah::searchAddress('Coblong Bandung Jawa Barat'); // Returns: ['province' => ..., 'regency' => ..., 'district' => ..., 'villages' => [...], 'confidence' => 0.92]
Pencarian Kode Pos
// Exact match Wilayah::postalCode('40172')->get(); // Wildcard (semua kode pos berawalan 401) Wilayah::postalCode('401*')->get(); // Dengan relasi hierarki (Province, Regency, District, Village sekaligus) $villages = Wilayah::searchByPostalCode('40172'); foreach ($villages as $v) { echo $v->district->name; // Cicendo echo $v->regency->name; // Kota Bandung echo $v->province->name; // Jawa Barat }
Hierarki & Alamat
// Muat hierarki dari kode apapun (desa, kecamatan, kab/kota, provinsi) $h = Wilayah::hierarchy('32.73.07.1001'); // Akses tiap level $h->village->name; // ARJUNA $h->district->name; // CICENDO $h->regency->name; // KOTA BANDUNG $h->province->name; // JAWA BARAT // Format alamat $h->toAddress(); // → "Kel. Arjuna, Kec. Cicendo, Kota Bandung, Jawa Barat 40172" $h->toShortAddress(); // → "Cicendo, Kota Bandung, Jawa Barat" // Semua level leluhur sebagai Collection $h->ancestors(); // → Collection [District, Regency, Province] // Dari kode kecamatan $h = Wilayah::hierarchy('32.73.07'); $h->province->name; // JAWA BARAT $h->village; // null (tidak ada jika kode bukan desa)
Dropdown & Select
// Format [code => name] — cocok untuk HTML <select> Wilayah::forDropdown('provinces'); // → ['11' => 'ACEH', '12' => 'SUMATERA UTARA', ...] Wilayah::forDropdown('regencies', province: '32'); // → ['32.01' => 'KAB. BOGOR', '32.73' => 'KOTA BANDUNG', ...] Wilayah::forDropdown('districts', regency: '32.73'); // → ['32.73.01' => 'ANDIR', '32.73.07' => 'CICENDO', ...] Wilayah::forDropdown('villages', district: '32.73.07'); // Format [{value, label}] — cocok untuk Livewire, Alpine.js, Vue, React Wilayah::forSelect('provinces'); // → [['value' => '32', 'label' => 'JAWA BARAT'], ...] Wilayah::forSelect('regencies', province: '32');
Paginasi
// Lengkap dengan link Wilayah::villages('32.73.07')->paginate(20); // Sederhana (prev/next only — lebih cepat) Wilayah::villages('32.73.07')->simplePaginate(20); // Cursor pagination (untuk infinite scroll) Wilayah::villages('32.73.07')->cursorPaginate(20); // Search + paginate Wilayah::search('Bandung')['regencies']->paginate(15);
Model & Relasi
use Aliziodev\Wilayah\Models\Province; use Aliziodev\Wilayah\Models\Regency; use Aliziodev\Wilayah\Models\Village; // Eager loading Province::with('regencies.districts.villages')->find('32'); // Relasi balik Village::where('code', '32.73.07.1001')->with('district.regency.province')->first(); // Scope bawaan Provincial::withCode('32')->first(); Regency::inProvince('32')->get(); District::inRegency('32.73')->get(); Village::withPostalCode('40172')->get();
Fitur Opsional
Aktifkan fitur opsional di config/wilayah.php:
'features' => [ 'islands' => true, // Data pulau (38.000+ pulau) 'areas' => true, // Luas wilayah per level 'populations' => true, // Data jumlah penduduk ],
Lalu jalankan seed:
php artisan wilayah:seed --with=islands
php artisan wilayah:seed --with=areas
php artisan wilayah:seed --with=populations
# Atau semua sekaligus
php artisan wilayah:seed --with=islands --with=areas --with=populations
Contoh penggunaan setelah aktif:
// Luas wilayah $province = Province::find('32'); $province->area?->area_km2; // 35.377,76 km² // Jumlah penduduk $province->population?->total; // 48.782.382
Artisan Commands
| Command | Keterangan |
|---|---|
wilayah:install |
Install otomatis (publish → migrate → seed) |
wilayah:seed |
Seed semua data ke database |
wilayah:seed --fresh |
Truncate lalu seed ulang |
wilayah:seed --province=32 |
Seed hanya satu provinsi (Jawa Barat) |
wilayah:seed --with=islands |
Seed data pulau (fitur opsional) |
wilayah:sync |
Sinkronisasi data dengan file terbaru (safe: upsert) |
wilayah:sync --dry-run |
Preview perubahan tanpa menerapkan |
wilayah:sync --province=32 |
Sync satu provinsi saja |
wilayah:version |
Cek versi data & hash upstream |
wilayah:cache-clear |
Hapus semua cache wilayah |
Update Data
Data upstream diperbarui otomatis oleh GitHub Actions setiap hari pukul 02:00 UTC. Workflow ini:
- memeriksa hash upstream dari
cahyadsn/wilayahdancahyadsn/wilayah_kodepos - menjalankan
normalize.phpuntuk memperbarui file didata/ - menaikkan patch version di
composer.json - membuat commit dan GitHub Release baru jika ada perubahan
Ketika ada rilis baru, cukup jalankan:
# 1. Update package composer update aliziodev/laravel-wilayah # 2. Preview dulu (opsional — untuk melihat perubahan) php artisan wilayah:sync --dry-run # 3. Terapkan php artisan wilayah:sync
Aman digunakan di production — Semua update menggunakan strategi
UPSERT:
- Data yang sudah ada tidak akan dihapus
- Hanya row baru yang di-insert dan nama yang berubah di-update
- Foreign key di tabel Anda tetap aman
Skema Update di CI/CD Internal
Upstream update wilayah / kodepos
→ GitHub Actions cek fingerprint upstream
→ normalize.php download & parse SQL
→ generate file data/* + version metadata
→ auto bump patch version
→ commit ke branch utama
→ GitHub Release baru
→ Packagist dapat mengambil versi terbaru
Catatan untuk Maintainer
- Workflow
sync-upstream.ymlmenggunakanGITHUB_TOKENbawaan GitHub Actions. SecretPAT_TOKENtidak diperlukan untuk konfigurasi default repo ini. - Jika branch utama memakai branch protection yang memblokir push dari GitHub Actions, step
git pushakan gagal sampai aturan repo mengizinkannya. - Workflow ini belum mengelola
CHANGELOG.mdsecara otomatis. Release dibuat dengan body markdown statis, bukan auto-changelog penuh.
API Dropdown Controller (Siap Pakai)
Package ini menyediakan WilayahController yang mempermudah Anda membuat API untuk nested dropdown (Provinsi -> Kota/Kab -> Kecamatan -> Kel/Desa) di frontend seperti Vue, React, Livewire, atau sekadar jQuery Ajax. Outputnya sudah terformat dalam standar [{ value: "id", label: "nama" }].
1. Daftarkan Route
Tambahkan definisi route ke dalam file routes/api.php di project Anda:
use Aliziodev\Wilayah\Http\Controllers\WilayahController; Route::prefix('wilayah')->group(function () { Route::get('provinces', [WilayahController::class, 'provinces']); Route::get('regencies', [WilayahController::class, 'regencies']); Route::get('districts', [WilayahController::class, 'districts']); Route::get('villages', [WilayahController::class, 'villages']); });
2. Implementasi Frontend (Contoh: Axios + Vanilla JS)
Berikut adalah contoh skrip sederhana menggunakan Axios dan Vanilla Javascript murni untuk menangani nested select-box:
<select id="provinsi"><option value="">Pilih Provinsi</option></select> <select id="kota" disabled><option value="">Pilih Kota/Kab</option></select> <select id="kecamatan" disabled><option value="">Pilih Kecamatan</option></select> <select id="desa" disabled><option value="">Pilih Desa</option></select> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script> document.addEventListener('DOMContentLoaded', () => { const elProvinsi = document.getElementById('provinsi'); const elKota = document.getElementById('kota'); const elKecamatan = document.getElementById('kecamatan'); const elDesa = document.getElementById('desa'); const fillSelect = (el, data) => { data.forEach(item => { const option = document.createElement('option'); option.value = item.value; option.textContent = item.label; el.appendChild(option); }); }; // 1. Load Provinsi axios.get('/api/wilayah/provinces').then(res => fillSelect(elProvinsi, res.data)); // 2. Load Kota/Kab elProvinsi.addEventListener('change', function() { const provId = this.value; elKota.innerHTML = '<option value="">Pilih Kota/Kab</option>'; elKota.disabled = !provId; elKecamatan.innerHTML = '<option value="">...</option>'; elKecamatan.disabled = true; elDesa.innerHTML = '<option value="">...</option>'; elDesa.disabled = true; if (provId) { axios.get(`/api/wilayah/regencies?province=${provId}`) .then(res => fillSelect(elKota, res.data)); } }); // 3. Load Kecamatan elKota.addEventListener('change', function() { const kotaId = this.value; elKecamatan.innerHTML = '<option value="">Pilih Kecamatan</option>'; elKecamatan.disabled = !kotaId; elDesa.innerHTML = '<option value="">...</option>'; elDesa.disabled = true; if (kotaId) { axios.get(`/api/wilayah/districts?regency=${kotaId}`) .then(res => fillSelect(elKecamatan, res.data)); } }); // 4. Load Desa/Kelurahan elKecamatan.addEventListener('change', function() { const kecId = this.value; elDesa.innerHTML = '<option value="">Pilih Desa</option>'; elDesa.disabled = !kecId; if (kecId) { axios.get(`/api/wilayah/villages?district=${kecId}`) .then(res => fillSelect(elDesa, res.data)); } }); }); </script>
Package Tambahan
🗺 Batas Wilayah (Polygon / GeoJSON)
composer require aliziodev/laravel-wilayah-boundaries php artisan vendor:publish --tag=wilayah-boundaries-migrations php artisan migrate php artisan boundaries:seed
use Aliziodev\WilayahBoundaries\Facades\Boundary; use Aliziodev\Wilayah\Models\Province; // 1. Menggunakan Facade $geojson = Boundary::forCode('32')->toGeoJson(); $collection = Boundary::collection(level: 1); // FeatureCollection // 2. Mengambil Wilayah beserta Boundary & Logo (BEST PRACTICE) // ✅ Gunakan eager loading (with) untuk mencegah N+1 query problem $province = Province::with('boundary')->find('32'); if ($province) { echo $province->name; // JAWA BARAT $logoUrl = $province->logoUrl(); // https://.../32.png $geojson = $province->boundary?->toGeoJson(); // Array GeoJSON (Polygon/MultiPolygon) $centroid = $province->boundary?->centroid(); // [lat, lng] }
🖼 Logo / Lambang Daerah
composer require aliziodev/laravel-wilayah-logos php artisan logos:publish
use Aliziodev\Wilayah\Models\Regency; // Ambil semua kabupaten di suatu provinsi lengkap dengan logonya $regencies = Regency::where('province_id', 32)->get(); $data = $regencies->map(function ($regency) { return [ 'code' => $regency->code, 'name' => $regency->name, // Macro logoUrl() dipanggil secara lazy, tidak menambah query DB 'logo' => $regency->logoUrl(), 'logo_thumb' => $regency->logoUrl('thumb'), ]; });
Di Blade:
<img src="{{ $province->logoUrl() }}" alt="{{ $province->name }}" width="80"> <img src="{{ $province->logoUrl('thumb') }}" alt="{{ $province->name }}" width="32">
Testing
composer install vendor/bin/pest
Untuk menjalankan test suite per fitur:
vendor/bin/pest --group=feature vendor/bin/pest tests/Feature/SearchTest.php vendor/bin/pest tests/Feature/HierarchyTest.php vendor/bin/pest tests/Feature/DropdownTest.php
Kontribusi
Pull request sangat diterima! Silakan buka issue terlebih dahulu untuk mendiskusikan perubahan yang ingin Anda buat.
Lisensi
MIT © Aliziodev
Kredit Data
Data wilayah bersumber dari:
- 🏛 cahyadsn/wilayah — Data 4 level wilayah administratif Indonesia
- 📮 cahyadsn/wilayah_kodepos — Data kode pos