Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Cities Laravel Package

ijeffro/laravel-cities

Laravel package that provides IATA codes, ISO 3166-3 and country codes for cities worldwide, with a migration generator and seeder to populate a searchable cities table in your Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Pick the right install path first. The repo currently carries two very different APIs, and the README only documents the old one.

Target Install Namespace / setup
Laravel 12/13, PHP 8.2+ (v1.0.0 rewrite) composer require ijeffro/laravel-cities:^1.0 FLAIRUK\Cities, auto-discovered — no config/app.php edits
Laravel 5.x legacy composer require ijeffro/laravel-cities:dev-master ijeffro\Cities\CitiesServiceProvider + Cities facade in config/app.php

If you are on a modern stack, ignore the README's provider/alias/migration instructions — they belong to the Laravel 5 bundle. Confirm with composer show ijeffro/laravel-cities before wiring anything.

2. Decide up front: in-memory only, or database-backed. The v1 package is usable with zero setup — lookups are served from a bundled dataset. The Eloquent table is optional and only worth it if you need SQL joins, reporting, or admin-editable rows.

# In-memory only
composer require ijeffro/laravel-cities:^1.0

# Optional: publish config/migration + seed
php artisan cities:install
php artisan migrate
php artisan cities:seed

3. Where to look first.

  • config/cities.php — table name and dataset wiring; publish it before migrating if you already own a cities table.
  • The City value object — readonly properties (code, name, country) are your real contract.
  • FLAIRUK\Cities\Rules\CityCode — the validation rule you'll use everywhere.
  • cities:install / cities:seed signatures — the fastest way to understand what gets published.

4. First use case (5 minutes). Validate an IATA city code coming off a form or API and echo the resolved city:

use FLAIRUK\Cities\Cities;

Cities::exists('LAX');          // true
$city = Cities::find('LAX');    // ?City — null when unknown
$city?->name;                   // "Los Angeles"
$city?->code;                   // "LAX"

Then plug it into a FormRequest:

public function rules(): array
{
    return [
        'origin'      => ['required', 'string', new CityCode()],
        'destination' => ['required', 'string', 'different:origin', new CityCode()],
    ];
}

Smoke-test in php artisan tinker before writing any application code — the dataset, not your code, is the variable here.

Implementation Patterns

Wrap the facade behind your own repository. This package has 11 stars and 0 dependents; you are the de-facto QA. A thin interface costs an hour and buys you the ability to swap the dataset later:

interface CityLookup
{
    public function find(string $code): ?City;
    public function search(string $term, int $limit = 10): Collection;
}

final class PackageCityLookup implements CityLookup
{
    public function find(string $code): ?City
    {
        return Cities::find(strtoupper(trim($code)));
    }

    public function search(string $term, int $limit = 10): Collection
    {
        return Cities::search($term)->take($limit);
    }
}

Pattern 1 — autocomplete endpoint. The single highest-value integration. Normalize, search, map to a stable JSON shape, cache:

public function __invoke(Request $request)
{
    $term = Str::of($request->string('q'))->trim()->upper()->limit(40, '');

    abort_if($term->length() < 2, 422, 'Query too short');

    $results = Cache::remember("cities:search:$term", now()->addHours(12), fn () =>
        $this->cities->search($term->toString(), 10)
            ->map(fn (City $c) => [
                'value' => $c->code,
                'label' => "{$c->name} ({$c->code})",
            ])->values()->all()
    );

    return response()->json($results);
}

Keep the cache keyed on the normalized term so lax and LAX share an entry.

Pattern 2 — scoped dropdowns. Use inCountry() + options() instead of hand-rolling country filtering:

// Country-scoped select in a booking form
$airports = Cities::inCountry('DE')->options(); // [code => label, ...]

// Cache the list — it's static per country
$options = Cache::rememberForever('cities:options:DE', fn () => Cities::inCountry('DE')->options());

options() is the right primitive for <select> builders; it saves you mapping objects to arrays in every view/component.

Pattern 3 — Eloquent joins when reporting matters. Publish the model + migration only if SQL does real work for you (e.g. orders joined to a city name for exports):

// Store the code, never the display name
Schema::table('orders', function (Blueprint $t) {
    $t->char('origin_code', 3)->index();
    $t->char('destination_code', 3)->index();
});

// Join for reporting
Order::query()
    ->leftJoin('cities as origin', 'origin.code', '=', 'orders.origin_code')
    ->select('orders.*', 'origin.name as origin_name')
    ->get();

Always persist the code and resolve display names at render time. Names change; codes (mostly) don't.

Pattern 4 — boundary validation, not business logic. Use the CityCode rule on every inbound payload (HTTP, console, queued jobs) and assume codes inside your domain are already valid:

Validator::make($payload, [
    'city' => ['required', new CityCode()],
])->validate();

Pattern 5 — graceful degradation in the API layer. findOrFail throws; catch it at the HTTP boundary and return a 422 with a helpful message rather than a 404 — the resource is the input, not the entity:

try {
    $city = Cities::findOrFail($request->validated('origin'));
} catch (\Throwable) {
    throw ValidationException::withMessages(['origin' => 'Unknown IATA city code.']);
}

Gotchas and Tips

The README is a trap. Provider registration, Cities alias, cities:migration, and CitiesSeeder are the Laravel 5 path only. On v1 everything is auto-discovered under FLAIRUK\Cities. Stale 'aliases' => ['Cities' => ijeffro\Cities\CitiesFacade::class] entries in config/app.php will fatal on a modern install — strip them during upgrade.

iso_3166_3 no longer exists in v1. It was renamed to code because the value is an IATA city code, not an ISO 3166-3 code. Any column, DTO, or query still referencing iso_3166_3 breaks. If you also store ISO country codes, name the columns unambiguously (iata_city_code vs country_code) — conflating the two is the classic bug in this domain.

City code ≠ airport code. Verify against your business definition before shipping. Multi-airport metros are exactly where this matters (one city code mapping to many airport codes); the package gives you city-level data, not a city→airport expansion. If you need "JFK/LGA/EWR for NYC", build that mapping yourself.

find() returns ?City — use it. findOrFail is convenient but throws a generic exception; in most apps find() + a validation message produces a far better UX and keeps the exception handler clean.

Readonly objects are not models. Don't save() a City, don't assume toArray() matches your table shape, and don't dd() the collection expecting Eloquent attributes. Treat them as immutable value objects and map to your own arrays at the edge.

Seeder is idempotent — migrations are not. php artisan cities:seed is safe to re-run after dataset updates. Never migrate:fresh --seed in production; re-run cities:seed alone.

Table-name collisions. If you already have a cities table, publish and edit config/cities.php before php artisan migrate, or point the config at a prefixed table (geo_cities). Check the published migration for indexes on code and country — add them if reporting queries are slow.

Search is not a geocoder. Expect straightforward string matching: no distance, no fuzzy ranking, no guaranteed transliteration. Test Zürich, São Paulo, and any non-Latin market you serve; normalize diacritics on input if matching feels brittle. If you need proximity or "near me", pair this with a geocoding API rather than stretching the package.

Data freshness is a composer problem. There is no live sync. Record which package version seeded your table (a dataset_version column or a config constant) so you can detect drift; schedule a quarterly review of pinned codes rather than assuming the dataset tracks IATA changes.

Pin and audit. Low-maintenance project: pin the exact tag in composer.json, run composer why-not ijeffro/laravel-cities ^1.0 when things explode, and read the diff of the published migration on every upgrade — it's small enough to review by hand.

Debugging loop. php artisan tinker → Cities::exists('...') first, then find(), then inCountry(). If lookups are empty, confirm which install path you actually took before blaming the dataset — that's the number-one time sink with this package.

Extension points, ranked. (1) Publish config for the table name and dataset wiring. (2) Publish the migration and add columns (timezone, lat, lng) — but keep your own columns in a separate table or a wrapper model so package upgrades don't clobber them. (3) Compose with the CityCode rule — write AirportCode or CountryCode rules alongside it rather than modifying the package's. (4) Fork only as a last resort; MIT and tiny means forking is cheap, but you inherit dataset maintenance forever.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log