seche/nova-laravel-world
A Laravel Nova package that brings world geographic data into your admin panel, with fields and resources for working with countries, states, and cities.
This is a reference-data package (countries, calling codes, divisions/regions, cities, and localized city names). The 1.0.1 release is bug-fix only — no new API surface — so treat it as the first stable baseline to build against rather than a feature upgrade.
composer require <vendor>/<package>:^1.0
1.0.0 shipped with a trailing comma in composer.json, which can break parsing/installation in stricter Composer versions. If you saw a composer.json parse error or were pinned to 1.0.0, upgrade immediately.php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider"
php artisan migrate
countries table is the most common "it's broken" report.Country, Division, City, CityLocale) and their relationships. That is the real documentation; the README tends to be thinner than the schema.First use case: a country dropdown backed by calling codes for a phone input, plus a city selector filtered by division:
$countries = Country::orderBy('name')->get(['id', 'name', 'iso2', 'callingcode']);
// In a Blade/Inertia form
<select name="country_id">
@foreach ($countries as $country)
<option value="{{ $country->id }}" data-calling-code="{{ $country->callingcode }}">
{{ $country->name }} (+{{ $country->callingcode }})
</option>
@endforeach
</select>
Store foreign keys, not display strings. Persist country_id / city_id (or ISO codes if the package exposes them as stable keys). Names and calling codes change; IDs and ISO codes don't. This also makes the 1.0.1 column fix a non-event for your data.
Wrap lookups in a repository or cached service. Reference data is read-heavy and effectively static. Cache the country list and per-country city lists with a long TTL and bust the cache only when you reseed:
class GeoRepository
{
public function countries(): Collection
{
return Cache::rememberForever('geo.countries', fn () =>
Country::orderBy('name')->get(['id', 'name', 'iso2', 'callingcode'])
);
}
public function citiesFor(int $countryId): Collection
{
return Cache::remember("geo.cities.{$countryId}", now()->addDay(), fn () =>
City::where('country_id', $countryId)->orderBy('name')->get()
);
}
}
Eager-load the chain before displaying localized city names. CityLocale::display() needs the city and its division; lazy loading that per row is a classic N+1 in city lists:
$cities = City::with(['division', 'locales'])->where('country_id', $id)->get();
$label = $city->localeFor(app()->getLocale())?->display();
Validate against the package, not against a hardcoded list. Use exists:countries,iso2 (or a small custom rule hitting the cached repository) in Form Requests. For phone numbers, derive the expected calling code from the selected country and compare, rather than shipping your own country table.
API resources over raw models. The tables carry columns (calling codes, locale rows) that are internal detail. Shape the payload explicitly so a future package migration doesn't silently change your public API.
Localization flow: pick the locale row in this order — exact match on app()->getLocale(), then a configured fallback (e.g. en), then the plain city name. Put that resolution in one place; don't repeat ?? chains across views.
Integration tip: if you only need countries and calling codes, don't migrate/seed the city and locale tables. Smaller footprint, faster CI, fewer moving parts to keep in sync.
->calling_code, a raw selectRaw, or a query referencing the old name, it will now fail. Grep your codebase for the old column before upgrading, and remove any as aliases you added to paper over it.CityLocale::display() now tolerates a null division. Previously it threw when Division Name was empty. The new behaviour returns a partial/possibly odd-looking string rather than erroring. Don't treat "no exception" as "correct output" — decide explicitly whether a city with no division should render as "City" or "City, Region" with a placeholder, and test with a deliberately division-less city.updateOrCreate or plain insert before running against a populated database. If it's insert, truncate first or you'll get duplicate-key failures.callingcode is a string, not an int. Country codes with leading zeros and multi-digit codes will bite you if you cast to integer or compare with == against a numeric input. Normalize by stripping + and whitespace on both sides.division_id as optional everywhere: in validation (nullable|exists:divisions,id), in UI (don't require a region for countries that don't use them), and in display logic.scopeActive, scopeWithCallingCode); publish the config for locale fallback and table name overrides; override the display formatter in a subclass rather than editing vendor files so future patch releases stay clean.How can I help you explore Laravel packages today?