open-afghanistan/provinces
Open Afghanistan Provinces is a Laravel/PHP package offering structured, ready-to-use data for Afghanistan's provinces, including names, codes, and helpful lookups for seeding databases and building location-aware applications. Lightweight and easy to integrate into any PHP project.
composer require open-afghanistan/provinces
The package is a data-oriented Laravel add-on: expect a service provider, a migration for provinces (and usually districts), a seeder, and optionally publishable config. Because it has no dependents and a small star count, treat the published files as your source of truth rather than the README — read the migration and seeder before you run anything.
Sequence for a first integration:
php artisan vendor:publish --provider="OpenAfghanistan\Provinces\ProvincesServiceProvider"
php artisan migrate
php artisan db:seed --class="OpenAfghanistan\Provinces\Database\Seeders\ProvincesSeeder"
Check config/provinces.php (if published) for table names, model class overrides, and locale settings; check the migration for column names (name, name_dari, name_pashto, code, foreign keys); check the seeder for whether IDs are hard-coded (this matters a lot — see Gotchas).
First use case: an address form. Pull provinces once, render a <select>, then load districts for the selected province via a small JSON endpoint. That single flow exercises models, relationships, validation, and caching — the four things this package touches.
If no service provider is auto-discovered, register it manually in bootstrap/providers.php (Laravel 11+) or config/app.php. Verify with php artisan about or a tinker query against the Province model before wiring UI.
Model access and relationships. Assume Province::with('districts')->get() and District::where('province_id', $id) as the core surface. Wrap these in your own repository or query scope rather than sprinkling package model calls through controllers — the package is small and unpinned semantically, so a thin adapter layer (App\Support\Geo) lets you swap or fork the data source later without touching views.
Cascading selects. Expose one route returning districts for a province:
Route::get('/geo/districts/{province}', fn (Province $province) =>
DistrictResource::collection($province->districts()->orderBy('name')->get())
);
Use route-model binding on the package model directly if its primary key is stable; otherwise bind by code with getRouteKeyName() on a subclass.
Validation. Prefer Rule::exists(Province::class, 'id') / Rule::exists(District::class, 'id') over string table names so table renaming via config doesn't break validation. Add a cross-field rule that the district actually belongs to the submitted province — the package won't enforce that for you:
'district_id' => [
'nullable',
Rule::exists('districts', 'id')->where('province_id', $request->province_id),
],
Caching. Province/district data is effectively static. Cache the full list forever and bust only on deploy:
$provinces = Cache::rememberForever('geo.provinces', fn () =>
Province::orderBy('name')->get(['id', 'name', 'code'])
);
For districts, cache per-province (geo.districts.{id}) to keep entries small and invalidatable individually. If you support both Dari and Pashto labels, cache per-locale.
Localized output. If the table carries name_dari / name_pashto, resolve at the resource layer via app()->getLocale() rather than duplicating rows. Add a fallback chain (name_{locale} → name) so a partially translated dataset never renders blanks in production.
Testing. Seed the package data in your test suite rather than hand-creating provinces, so ID assumptions match production:
protected function setUp(): void {
parent::setUp();
$this->seed(\OpenAfghanistan\Provinces\Database\Seeders\ProvincesSeeder::class);
}
If the seeder is slow or noisy, snapshot the two tables into a fixture and insert() them in setUp under RefreshDatabase.
API surface. If you expose geo data publicly, version it (/api/v1/geo/...) — the dataset's shape (ISO codes, district counts) is a compatibility concern for mobile clients regardless of what the package does.
id plus code, or denormalize the name onto your address record.code vs id. Afghanistan uses ISO 3166-2 (AF-KAB, AF-HER, …). Validate against code if you need stability across reseeding; join on id internally. Confirm the seeder actually populates code before relying on it.db:seed run twice often fails on duplicate keys or silently doubles rows if it uses insert() without truncation. Wrap in truncate()/upsert() in your own wrapper seeder, and gate it behind --force in production deploys.province_id, code unique) and any column changes. Otherwise migrate on a fresh clone diverges from existing environments.U+200C) and Arabic presentation variants. Search, dedupe, and title-casing will misbehave unless you normalize (NFKC + strip ZWNJ) before comparing."open-afghanistan/provinces": "1.x"). With zero dependents, you are effectively the first integration test; budget time to read the source.geo_sources note or a config flag, and plan for admin-editable records (or a custom table you control) if accuracy is business-critical.restrictOnDelete or nullable province_id — a cascading delete from a reseeded geo table could take your addresses with it. Safest: no FK at all, validate in the app layer.Province, add scopes/accessors, and point config at your subclass instead of forking the package.geo.provinces.v{appVersion}) so a dataset update actually surfaces.How can I help you explore Laravel packages today?