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 Airports Laravel Package

ijeffro/laravel-airports

Laravel Airports provides IATA codes, ISO 3166-3 and country codes for airports, plus a service provider, facade, migration and seeder to populate an airports table. Install via Composer, publish config, migrate and seed for searchable airport data.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm which package you're actually installing. The repository carries two generations of docs: the original ijeffro/laravel-airports (Laravel 5, dev-master, manual provider/alias registration) and the v1.0.0 rewrite (FLAIRUK\Airports, PHP 8.2+, Laravel 12/13, auto-discovery). They are not interchangeable — the namespace, service provider, and API all changed. Pick one and stick to it:

# New line (Laravel 12/13)
composer require flairuk/laravel-airports

# Legacy line (Laravel 5–11, pinned)
composer require ijeffro/laravel-airports:dev-master

Check exactly what landed before writing code:

composer show flairuk/laravel-airports   # or ijeffro/laravel-airports

2. Skip the database on day one. The whole point of the v1.0.0 rewrite is that lookups are an in-memory PHP array — no migration, no seeding, no cache. Your first useful line is:

use FLAIRUK\Airports\Facades\Airports;

$airport = Airports::find('JFK');   // ?Airport (readonly object, not an array)

$airport?->iata;    // 'JFK'
$airport?->name;    // 'John F Kennedy Intl'
$airport?->city;    // 'New York'
$airport?->country; // 'US'

3. First real use case: form validation. If you've been hand-rolling exists:airports,iata or a regex, swap in the shipped rule — it is backed by the in-memory dataset and therefore works with zero infrastructure:

use FLAIRUK\Airports\Rules\AirportCode;

$request->validate([
    'departure_iata' => ['required', 'string', new AirportCode()],
    'arrival_iata'   => ['required', 'string', new AirportCode()],
]);

4. Add the table only when you have a reason. If you need SQL JOINs, aggregates, or FK constraints on departure_iata, then:

php artisan vendor:publish --tag=airports-migrations
php artisan airports:install   # migrate + idempotent seed

5. Where to look first in the codebase. The README is thin on signatures, so read the facade/service class and the Airport value object directly (vendor/flairuk/laravel-airports/src/) and confirm method names in php artisan tinker before spreading them across controllers. Treat the documented surface (find, findOrFail, exists, inCountry, search, options) as the intended API and verify each one you depend on.


Implementation Patterns

Lookup API as your default read path

All of these are pure array operations against a compiled dataset — no I/O, no cache layer:

Call Returns Use for
find('LAX') ?Airport Optional lookups, display hydration
findOrFail('LAX') Airport or throws Importers, queued jobs — fail loudly on bad data
exists('LAX') bool Guard clauses, pre-flight validation of bulk input
inCountry('GB') collection of Airport Country-scoped selects, regional reporting
search('Heathrow') collection Typeahead / autocomplete endpoints
options() ['JFK' => 'John F Kennedy Intl (New York)', ...] <select> inputs

Because there's no query cost, delete the Cache::remember() wrappers you'd normally put around airport dropdowns. They add invalidation bugs for no gain. Cache only at the HTTP layer when you serialize large lists into an API response.

Normalize at the boundary, once

IATA codes are conventionally uppercase; user input is not. Do it in the form request, not scattered around:

protected function prepareForValidation(): void
{
    $this->merge([
        'departure_iata' => strtoupper(trim((string) $this->departure_iata)),
        'arrival_iata'   => strtoupper(trim((string) $this->arrival_iata)),
    ]);
}

Autocomplete endpoint

public function __invoke(Request $request)
{
    $term = trim((string) $request->string('q'));

    if (strlen($term) < 2) {
        return AirportResource::collection(collect());
    }

    return AirportResource::collection(
        Airports::search($term)->take(15)
    );
}

Bound the result set before it crosses the wire — search() on a large dataset can return thousands of matches, and options() returns the full list (~10k entries). Never render or JSON-encode options() unfiltered on a public page; filter by country or city first.

Readonly value objects: wrap, don't mutate

The Airport object is readonly. Every 0.x array idiom ($airport['city'], ->merge(), ->toArray()) breaks. When you need computed or localized fields, decorate:

final class AirportView
{
    public function __construct(
        public readonly Airport $airport,
        public readonly ?float $distanceKm = null,
    ) {}

    public function label(): string
    {
        return "{$this->airport->name} ({$this->airport->iata})";
    }
}

Bulk import pipelines — validate the whole file first

The classic failure is a half-written import where row 400 has a typo. Use the in-memory index as a cheap pre-check before touching the DB:

$codes = collect($rows)
    ->pluck('iata')
    ->map(fn ($c) => strtoupper(trim((string) $c)));

$unknown = $codes->reject(fn ($c) => Airports::exists($c))->unique()->values();

abort_if($unknown->isNotEmpty(), 422, 'Unknown IATA codes: '.$unknown->implode(', '));

Wrap this in a custom rule or a form request so it also runs for API clients.

Eloquent, when you opt in

With the migration published you get a model suitable for joins:

public function departureAirport()
{
    return $this->belongsTo(\FLAIRUK\Airports\Models\Airport::class, 'departure_iata', 'iata');
}

Rule of thumb: Eloquent for sets (whereIn, joins, GROUP BY country reports), facade for single codes in domain logic. Don't alternate between the two for the same decision — they are separate sources of truth.

Seeding and deploys

Prefer the dedicated command over calling the seeder class manually:

php artisan airports:seed   # documented as idempotent — safe on every deploy

Put it after migrate in your deploy script. If you add rows your app owns (disabled airports, custom display names), keep them in your table or a pivot, not in the package table — see Gotchas.

Testing

Domain tests need no database at all:

it('rejects unknown airports', function () {
    expect(Airports::exists('ZZZ'))->toBeFalse();
});

Add RefreshDatabase only to feature tests that touch the published table or the Eloquent model. Keep a fixture of a handful of known-good codes (JFK, LHR, NRT) rather than asserting on dataset row counts — the dataset will drift between releases.


Gotchas and Tips

1. The two READMEs describe different packages. Before you copy any snippet, note the namespace: ijeffro\Airports\AirportsServiceProvider vs FLAIRUK\Airports\.... On v1.0.0, remove any legacy providers/aliases entries from config/app.php — a cached config pointing at the old namespace produces "class not found" errors that survive composer update. After upgrading:

composer dump-autoload
php artisan config:clear && php artisan cache:clear

2. Objects, not arrays. This is the #1 breakage when migrating from 0.x. Grep for getByIata, getByName, whereCountry, ['city'], and ->merge( across your codebase and convert them to find() plus null-safe property access.

3. Case and whitespace. find('jfk') / find(' JFK') may not resolve. Normalize once at the request boundary — cheapest place to fix a bug that's otherwise annoying to trace.

4. Sparse records exist. city, region, and coordinates can be null or empty on smaller airfields. Always ?-> / ?? '—' in Blade, API resources, and any distance calc — a null latitude will blow up a haversine.

5. Two sources of truth can drift. The in-memory array and the published airports table are independent. airports:seed refreshes the table; if you've edited rows locally, a re-seed may overwrite them. Pick one as authoritative per use case and write it down — the facade for reference data, the table for anything the app mutates.

6. Memory and opcache in long-lived processes. The dataset ships as an opcache-friendly PHP array — fast, but not weightless. Under Octane or many queue workers, each process holds its own copy. Usually fine; measure if you run dozens of workers per box. Also avoid json_encode(Airports::options()) on every request.

7. Validation failing on a valid code. Distinguish "not in the dataset" from "wrong class resolved". Test directly in tinker:

Airports::exists('JFK');   // true = dataset fine, look at your rule binding

8. Table/command problems. If airports:seed reports success but queries return nothing, check three things in order: the migration actually published (tag names have changed between releases), the config table name matches what you publish, and your belongsTo foreign key matches the actual column — inspect the schema rather than trusting 0.x-era column guesses (region may not exist).

9. Facade binding errors. If Airports::find() throws, either the alias doesn't match the installed FQCN or auto-discovery is disabled (dont-discover in composer.json). Fall back to constructor-injecting the underlying service class, or register the provider explicitly. Injecting a thin wrapper service is the better long-term pattern anyway — it keeps the vendor namespace out of your controllers and gives you a seam for tests.

10. Extension points, in preference order:

  • Wrap, don't subclass — readonly Airport objects compose cleanly into DTOs, API resources, and view models.
  • Localization — keep translations keyed by IATA in your own lang/ files and resolve at render time; never rewrite the package dataset.
  • Custom resolvers — for multi-tenant or region-restricted apps, build a service that composes inCountry()/search() with your authorization scope and inject that, so the facade appears in one place.
  • Data refresh — if you need newer airport data than shipped, either regenerate a PHP array in the package's format, or keep the table and seed from OpenFlights in a scheduled job. Don't hand-patch vendor/.

11. Legacy stay-or-go. For Laravel 5–11 there is no in-place upgrade path to v1.0.0. Either stay pinned on the old dev-master (accepting the facade/array API and Laravel 5-only support), or budget a real migration: namespace swap, provider removal, array→object call-site rewrite, and re-validation of every exists:airports,iata rule.

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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc