dipeshsukhia/laravel-country-state-city-data
Laravel package that ships ready-to-use country, state, and city data with migrations, Eloquent models, and seeders. Add searchable location dropdowns and relational geography to your app quickly, without manually importing or maintaining large datasets.
Install the package and pull in its data layer:
composer require dipeshsukhia/laravel-country-state-city-data
Publish the package assets (config, migrations, and optionally models/seeders). Use the vendor publish tag documented in the README, e.g.:
php artisan vendor:publish --tag=country-state-city
Then migrate and seed the world data:
php artisan migrate
php artisan db:seed --class="DipeshSukhia\\CountryStateCity\\Seeders\\CountryStateCitySeeder"
If the package provides a dedicated command, prefer that—check php artisan list | grep country after install.
Where to look first
config/country-state-city.php — table names, model bindings, and any toggles.database/migrations/ — table structure, indexes, and foreign-key behavior.src/Models/ — Country, State, City relationships and fillable fields.First use case
A dependent country → state → city dropdown is the canonical day-one use case:
use DipeshSukhia\CountryStateCity\Models\Country;
use DipeshSukhia\CountryStateCity\Models\State;
$countries = Country::orderBy('name')->pluck('name', 'id');
$states = State::where('country_id', $countryId)
->orderBy('name')
->pluck('name', 'id');
Use those collections directly in Blade select elements, or return them as JSON for AJAX-driven forms.
Use the Eloquent models directly for reads. The package is primarily a data provider, so most integration is query-level. Eager-load relationships when you need nested data:
$country = Country::with(['states' => fn ($q) => $q->orderBy('name')])
->findOrFail($countryId);
Build dependent dropdown endpoints. For address forms, expose lightweight JSON endpoints instead of rendering thousands of options server-side:
Route::get('/states', function (Request $request) {
return State::where('country_id', $request->integer('country_id'))
->orderBy('name')
->get(['id', 'name']);
});
Pair this with a small Alpine/vanilla JS handler that clears the city select when the state changes.
Cache reference data aggressively. Country/state/city lists change rarely, so cache them with a versioned key:
$states = Cache::remember("states:country:{$countryId}", now()->addDay(), function () use ($countryId) {
return State::where('country_id', $countryId)->orderBy('name')->pluck('name', 'id');
});
Clear these keys after any re-seed or data sync.
Store foreign keys, not names. In your own addresses table, keep country_id, state_id, and city_id columns with constrained() foreign keys. This makes filtering and reporting reliable and avoids name-matching bugs (e.g., “Georgia” the country vs. the U.S. state).
Validate against the package tables. Use Laravel’s exists rule with the configured table names:
$request->validate([
'country_id' => ['required', 'exists:countries,id'],
'state_id' => ['required', 'exists:states,id'],
'city_id' => ['nullable', 'exists:cities,id'],
]);
For stricter integrity, add a custom rule that verifies the selected state belongs to the selected country and the city belongs to the state.
Seed only what you need in tests. The full dataset is large. In test suites, either use a small fixture set or seed specific countries:
Country::factory()->create(['name' => 'Testland']);
State::factory()->create(['country_id' => $country->id]);
If factories aren’t shipped, insert minimal rows directly in a test seeder.
Integrate with form components. For Blade, Livewire, Filament, or Nova forms, wrap the queries in small services or view composers. Keep the package models behind your own repository if you expect to swap data sources later:
class LocationRepository
{
public function countries(): Collection
{
return Cache::rememberForever('countries', fn () => Country::orderBy('name')->pluck('name', 'id'));
}
}
Use AJAX search for cities. Never render all cities for a country. Search by name with a limit:
City::where('state_id', $stateId)
->when($term, fn ($q) => $q->where('name', 'like', "{$term}%"))
->orderBy('name')
->limit(50)
->get(['id', 'name']);
php -d memory_limit=-1 artisan db:seed ...) and expect several minutes. Avoid running the full seeder during every test run.states.country_id and cities.state_id are indexed. If you publish and modify migrations, add indexes on any columns you filter by (name, foreign keys).City::all() in production. It will bloat memory and slow responses. Always constrain by state/country and select only id and name.countries, states, or cities tables, change the package table names in config before migrating, or you’ll collide.cascadeOnDelete, deleting a country will wipe its states and cities. For address records, prefer restrictOnDelete or nullOnDelete in your own tables.Cache::tags if your store supports it.country_id/state_id as integers and verify parent-child relationships server-side. Don’t trust the client to send consistent IDs.DB::enableQueryLog()) or Laravel Debugbar and look for accidental full-table scans. The usual culprit is loading all cities or missing indexes.$table to the published table names, or wrap queries in your own repository. Check config/country-state-city.php for overridable model classes before duplicating logic.How can I help you explore Laravel packages today?