khsing/world
khsing/world gives Laravel apps a ready-made database of countries, states, cities, currencies, timezones and languages, with migrations, seeders, Eloquent models and lookup helpers so you can add reliable geographic and locale data without building it yourself.
The package ships three tables (countries, states, cities), Eloquent models for each, and a bundled dataset you install into your own database — nothing is queried remotely at runtime.
composer require khsing/world
On Laravel 5.5+ the Khsing\World\WorldServiceProvider is auto-discovered. If you're on an older skeleton (or a non-standard bootstrap), register the provider and the optional facade alias in config/app.php.
Then publish the config/migrations and run the installer, which migrates and seeds the geography data:
php artisan vendor:publish --provider="Khsing\World\WorldServiceProvider"
php artisan world:install
Sanity check the result before wiring anything up:
php artisan tinker
>>> Khsing\World\Models\Country::count(); // ~250
>>> Khsing\World\Models\Country::getByCode('US')->states()->count();
>>> Khsing\World\Models\City::where('name', 'Shanghai')->with('state.country')->first();
First use case: a three-level cascading address picker. Add country_id / state_id / city_id to your addresses table, populate the country <select> from Country::orderBy('name')->get(['id','name','cca2']), and fetch states/cities from two tiny JSON endpoints using the model relations. That's 90% of what this package is used for, and it's worth doing that one flow end-to-end before touching anything else.
Swap in your own models via config, don't fork. config/world.php exposes a models map. Subclass the package model, add your scopes/accessors, and point the config at it. Because State resolves Country and City resolves State through the container/config, your subclass is used everywhere — including inside relations — as long as you register it before any models are hydrated.
namespace App\Models;
class Country extends \Khsing\World\Models\Country
{
public function scopeActive($q) { return $q->where('ee', '!=', ''); }
public function flagUrl(): string
{
return "https://flagcdn.com/w80/".strtolower($this->cca2).".png";
}
}
// config/world.php
'models' => [
'country' => App\Models\Country::class,
'state' => App\Models\State::class,
'city' => App\Models\City::class,
],
Cascade endpoints should be narrow and cached. Countries change essentially never; states change rarely; cities change rarely but there are ~140k of them. Cache the country list forever and bust on deploy:
Route::get('/geo/countries', fn () => Cache::rememberForever('geo.countries',
fn () => Country::orderBy('name')->get(['id','name','cca2'])
));
Route::get('/geo/states/{country}', function (Country $country) {
return Cache::remember("geo.states.{$country->id}", now()->addDay(),
fn () => $country->states()->orderBy('name')->get(['id','name'])
);
});
Route::get('/geo/cities/{state}', function (State $state) {
return $state->cities()->orderBy('name')->get(['id','name']); // no cache: too many keys
});
Eager load and select columns. A naive City::with('state.country')->get() on the full table will blow memory. Always paginate or scope, and select only what the view needs:
$cities = City::query()
->where('state_id', $stateId)
->orderBy('name')
->paginate(50, ['id', 'name', 'state_id']);
Store IDs, resolve names at render time. Persist country_id, state_id, city_id on your own model and keep the FKs (with nullOnDelete off — the geo tables shouldn't be truncated in production). Denormalizing the string name is tempting but breaks the moment you switch locales or the dataset is refreshed.
Validation: use exists rules against the package tables directly rather than trusting the posted cascade, because a client can post a city_id from a different state.
$data = $request->validate([
'country_id' => ['required', 'exists:countries,id'],
'state_id' => ['nullable', 'exists:states,id'],
'city_id' => ['nullable', Rule::exists('cities', 'id')
->where('state_id', $request->input('state_id'))],
]);
Seeding in a deploy pipeline. Treat world:install as a one-time, out-of-band operation — not part of php artisan migrate in every deploy. Run it in a release step with --force, inside a transaction where the driver supports it, and never in a request or queue job where PHP's max_execution_time applies.
php artisan world:install --force
The seed is the whole ballgame. ~140k city rows plus states will take minutes and hundreds of MB of RAM on a default PHP CLI config if it's doing row-by-row inserts. Run it with php -d memory_limit=1G artisan world:install, and on MySQL consider raising max_allowed_packet and temporarily setting innodb_flush_log_at_trx_commit = 2. It is also worth checking whether the installer wraps the load in a transaction — a half-seeded cities table is indistinguishable from a country that genuinely has no cities.
Migrations are published, not vendor-locked. That's a feature and a trap: you can add indexes or extra columns, but re-publishing will overwrite your edits, and the installer may no-op if it thinks migrations already ran. If you need a schema change, add your own migration on top instead of editing the published ones.
Table name collisions. If you already have countries/states/cities (or a states table for US states only), audit before installing. Renaming via config is not supported; you'd have to override $table on your subclasses and re-point the relations.
Data gaps and nulls are normal. Plenty of countries have no state-level rows, and some states have no cities. Design the UI so state/city are optional and the cascade degrades gracefully, and always ?->/optional() the $city->state->country chain in views. Also expect naming variance — "state" is used for provinces, prefectures, oblasts, and emirates alike, so label it "Region"/"Province" in your UI if your audience is international.
Config caching hides model swaps. In production with config:cache, changing the models map or the locale requires a cache clear and a full redeploy. Symptom: your subclass methods work locally and 500 in prod with "method does not exist."
Locale handling. The config exposes a locale, but the shipped dataset is only as complete as the source data for that locale. Test a few well-known countries rather than assuming every name is translated; fall back to the English name when the localized column is empty.
Testing. Never run the full seeder in a test suite. Either use RefreshDatabase against a pre-seeded SQLite fixture committed to the repo, or seed a 3-country subset in a test-only seeder. A Country::factory() doesn't exist — create records with Country::create([...]) in your test helpers.
Extension points worth knowing: the models config map is the primary one; after that, model subclasses let you add scopes, accessors (flag URLs, full_name fallbacks), and relations. There's no event system or repository layer, so keep the package at the persistence edge of your app and put any business logic in your own services — that way a future swap to a different dataset is a one-class change, not a grep across the codebase.
How can I help you explore Laravel packages today?