visiosoft/location-module
Laravel location module for managing countries, regions, and cities in Visiosoft applications. Provides geographic models and migration-ready structures for adding hierarchical location data and selectors, helping organize and reference location records across your project.
Prerequisites: a Laravel app running visiosoft/streams-platform (PyroCMS 2.x, OpenClassify, or a custom Streams install). This is not a standalone Laravel package — it depends on Streams' addon system, field types, and repository contracts. If you only need country/city data in vanilla Laravel, look at laravel-world / squire instead.
Install:
composer require visiosoft/location-module
php artisan addon:install visiosoft.module.location # runs migrations + seeders
If addon:install isn't available, register Visiosoft\LocationModule\LocationModuleServiceProvider in config/app.php, then:
php artisan migrate
php artisan db:seed --class="Visiosoft\LocationModule\Country\CountrySeeder"
Where to look first:
src/LocationModuleServiceProvider.php — bindings for repositories and the addon registration.src/Country/, src/State/, src/City/ (or src/Locations/) — each contains a *Model, *Repository + Contract/*RepositoryInterface, and a *Seeder. This trio is the pattern everywhere in the module.migrations/ — confirms column names and FK order before you write joins.resources/config/ and resources/lang/ — table names and any labels.First use case: a cascading country → state → city select on a listing form, and filtering ads by location afterward.
use Visiosoft\LocationModule\Country\Contract\CountryRepositoryInterface;
public function create(CountryRepositoryInterface $countries)
{
return view('listing.create', ['countries' => $countries->all()]);
}
Resolve repositories, not models. The module binds *RepositoryInterface in its service provider and uses them for caching, pagination, and finders. Injecting CountryModel directly bypasses that logic.
public function __construct(
private StateRepositoryInterface $states,
private CityRepositoryInterface $cities,
) {}
Relationships are standard Eloquent. Country::find($id)->states (hasMany), State::find($id)->cities (hasMany). Use ->with('states') when you need them together.
Cascading select endpoints. Keep payloads small ({id, name}) and cache per parent:
Route::get('/api/countries/{country}/states', fn ($country) =>
Cache::remember("states.$country", 3600, fn () =>
app(StateRepositoryInterface::class)->findByCountry($country)
)
);
Route::get('/api/states/{state}/cities', fn ($state) =>
app(CityRepositoryInterface::class)->findByState($state)
);
Front-end: fetch states on country change, fetch cities on state change, clear downstream selects.
Filtering listings by location. Filter at the most specific level the user chose, with a fall-back to the parent:
Ad::query()
->when($request->city_id, fn ($q) => $q->where('city_id', $request->city_id))
->when($request->state_id && ! $request->city_id,
fn ($q) => $q->where('state_id', $request->state_id))
->when($request->country_id && ! $request->state_id,
fn ($q) => $q->where('country_id', $request->country_id))
->paginate();
Seed once, in deployment only. Country/state/city datasets are large. Use targeted seeders in migrations and a tiny fixture set in tests (RefreshDatabase + a handful of hand-created countries), never the full seeder.
Extending via the service provider. To add ISO codes, currency, timezone, or a translated name:
$this->app->bind(CountryRepositoryInterface::class, MyCountryRepository::class);
// or extend the model
Streams field type integration. When used as a Streams field type, assign it to a form and read ->getValue() — it returns the resolved model/id depending on config. Prefer the field type over hand-rolled selects inside Streams forms.
streams-platform and its providers/config. Validate that cost before choosing it over a static location package.ini_set('memory_limit', '-1'), use chunked inserts, and never leave --seed in a routine migrate deploy step.migrate --seed trap. It re-runs all seeders on every deploy; on the city table this is slow and can duplicate rows if the seeder isn't idempotent.states must migrate after countries, cities after states. On MySQL 5.7, check index length on name columns (utf8mb4 → 191 chars) or the index creation fails.countries, you'll get duplicate-table errors; publish and rename the migrations.city.state_id is restrictOnDelete, deleting a state fails silently. Add ->onDelete('cascade') in your own migrations where appropriate.all()/findByCountry won't notice edits or imports. Cache::forget("states.$countryId") after any write, or call Cache::flush() post-import.locations_translations table plus an accessor if you need multilingual labels.country.code or country.currency exists; inspect the migration first.php artisan addon:list | grep -i location, then dump(app(CountryRepositoryInterface::class)) to confirm the binding resolved. Check storage/logs/laravel.log for seeder FK failures.vendor/.How can I help you explore Laravel packages today?