altwaireb/laravel-world
Laravel World provides a complete, ready-to-use database of countries, states, cities, currencies, languages, timezones, and more. It ships with migrations, seeders, models, caching, and multilingual support, so you can add geo data to any Laravel app in minutes.
Minimal path from install to first useful query:
composer require altwaireb/laravel-world
# Publish config, migrations, seeders (and optionally lang files)
php artisan vendor:publish --tag=world
# Create tables
php artisan migrate
# Load the dataset (large — see Gotchas)
php artisan world:seed
# If your version has no world:seed command, fall back to:
# php artisan db:seed --class=WorldSeeder
Pin the right major for your framework. v2.0.0 is the first release with Laravel 13 support (CI starts at 13.38.0) and still supports Laravel 10, 11 and 12. On Laravel 13 you need ^2.0; on 10–12 either major works, but prefer v2 for the streaming seeder:
composer require altwaireb/laravel-world:^2.0
composer show altwaireb/laravel-world # confirm the major you actually got
Check what your installed version actually exposes before scripting anything:
php artisan list | grep -i world
Where to look first (in this order):
config/world.php — the single most important file. It controls caching, route/API registration, table naming, custom fields, and which resources get seeded.Altwaireb\World\Models\Country, State, City — read the $fillable, $casts, relationships, and scopes. This tells you the real column names (iso2, iso3, phone_code, currency, state_code, country_code, etc.) faster than any README.src/Commands and database/seeders — confirms seed order, whether re-runs are idempotent, and (v2+) that the JSON source is read via a stream rather than json_decode(file_get_contents(...)).First use case — an address form or a country lookup:
use Altwaireb\World\Models\Country;
// Country dropdown
$countries = Country::orderBy('name')->pluck('name', 'id');
// Resolve by ISO code for phone/currency defaults
$country = Country::where('iso2', 'US')->firstOrFail();
$currency = $country->currency;
// Cascading selects
$states = $country->states()->orderBy('name')->get(['id', 'name']);
$cities = \Altwaireb\World\Models\City::where('state_id', $stateId)
->orderBy('name')
->get(['id', 'name']);
Store foreign keys, not strings. The highest-value pattern is to keep country_id, state_id, city_id on your addresses/users tables and let the package own the reference data. Add the FKs with nullOnDelete or no cascade — you do not want a reseed wiping user rows.
Schema::table('addresses', function (Blueprint $table) {
$table->foreignId('country_id')->nullable()->constrained('countries');
$table->foreignId('state_id')->nullable()->constrained('states');
$table->foreignId('city_id')->nullable()->constrained('cities');
$table->string('postal_code')->nullable();
});
Cascading form endpoint. Keep the controller thin and return only what the UI needs:
public function states(Country $country)
{
return $country->states()->orderBy('name')->get(['id', 'name', 'state_code']);
}
public function cities(State $state)
{
return $state->cities()->orderBy('name')->get(['id', 'name']);
}
Bind by iso2 in URLs if you want readable routes (/geo/US/states), but keep numeric IDs in storage.
Validation via Rule::exists. Never trust a client-submitted state_id independent of country_id; validate the pairing:
$data = $request->validate([
'country_id' => ['required', Rule::exists('countries', 'id')],
'state_id' => ['required', Rule::exists('states', 'id')->where('country_id', $request->country_id)],
'city_id' => ['nullable', Rule::exists('cities', 'id')->where('state_id', $request->state_id)],
]);
Cache the dropdowns, not the world. Countries and states are small and stable; cities are the expensive part. Cache per-country or per-state slices rather than one giant array:
$states = Cache::remember("world.states.{$countryId}", now()->addDay(), fn () =>
State::where('country_id', $countryId)->orderBy('name')->get(['id', 'name'])->toArray()
);
If the package's built-in cache is enabled in config/world.php, verify its store and TTL and decide whether you want two cache layers. For most apps, disabling the package cache and caching at the query level you control is easier to reason about.
Custom fields. If you need population, flag, is_active, etc., add them through the config's custom-field mechanism (and publish/re-run the migration) rather than editing vendor migrations. Re-seeding then populates them. Re-publish with --force only on a fresh DB — otherwise you will silently overwrite your own edits to published files.
Don't fight the stream in v2. The seeder now reads the JSON incrementally (halaxa/json-machine), so any local override that assumed a fully-hydrated in-memory dataset — collecting all rows, sorting them in PHP, cross-referencing countries against cities before insert — is the wrong shape now. Do lookups and ordering in SQL/Eloquent after seeding, or keep your own importer separate from the package seeder.
Tests. Don't seed the full dataset in CI. Seed a trimmed subset if the config/seeder supports it, or insert three countries and a handful of states/cities in a factory. Use RefreshDatabase and assert the relationship chain, not the row counts. Lower seeder memory (v2) makes full seeding less dangerous, but it does not make it fast — keep CI trimmed.
API routes. If config/world.php has API registration, enable it behind auth/throttle middleware and confirm pagination is on. Exposing a 150k-row cities index unauthenticated and unpaginated is a foot-gun. For most apps, writing two or three of your own controllers is cheaper than configuring the bundled routes.
Upgrades. v2.0.0 is a major bump, though the release notes describe only additive changes (streaming reader, Laravel 13 support). Still treat the dataset as mutable reference data, not immutable constants: re-publish config/migrations, diff them, re-run the seeder on a staging copy, and check that the IDs your users reference still exist. Keeping iso2/state_code alongside the FK makes later reconciliation possible. If you are moving 1.x → 2.x, pin, seed a staging copy, and compare row counts plus a few spot-checked iso2/state_code pairs before touching production. On Laravel 13, verify your other packages support it — the world package shipping 13 support does not mean your auth or admin panel does.
composer show (you may be on 1.x) and remember Eloquent model hydration during inserts can dominate memory even with a streamed reader.halaxa/json-machine. If your org runs license/security scanning or vendors a fixed dependency allowlist, whitelist it before upgrading; it is a pure-PHP reader, so no new extension requirements beyond ext-json.insert or updateOrCreate. On a non-fresh DB, prefer truncating the three tables first (after confirming nothing references them) or running against a clean schema.config:cache bites env-driven config. Values read from env() inside config/world.php are frozen after php artisan config:cache. Change the env var, then re-cache, or the seeder/cache settings appear to be ignored.countries/states/cities tables, publishing will collide. Check the config for table-name overrides, or rename yours before migrating.Country::with('states.cities')->get() will attempt to hydrate every city. Always scope to one country/state, select explicit columns, and paginate anything city-level.name. Always join/store IDs, and expect duplicate city names across states and countries.native is often a better display value for non-English audiences. Don't assume translations exist for every row — fall back explicitly.--force overwrites your published copies. If you customize migrations or config, note it in the repo so the next developer doesn't blindly re-publish. Re-check published config after a major bump — new keys (cache/streaming related) may have appeared and silently default to off.country_id/state_id first, then confirm the seeder actually ran and completed for that country. With v2's incremental writes, a killed seed is the likeliest cause — not missing source data.world.states.{countryId}) so a reseed of one country doesn't require flushing everything. Tag-based flushing only works with Redis/Memcached, not the file or database store.class Country extends \Altwaireb\World\Models\Country) and point your relationships at the subclass if you need extra behavior, fillable fields, or scopes — that keeps composer update from stomping your logic. If you fork the seeder to add custom fields, prefer a separate command you own over editing the vendor one.How can I help you explore Laravel packages today?