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

woodsandwalker/laravel-countries

Laravel Countries makes it easy to add country data to Laravel apps with migrations, Eloquent models, and helpers for querying countries by name or ISO code. Use it to populate select menus, validate country input, and build location-aware features without maintaining your own country list.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and inspect before publishing.

composer require woodsandwalker/laravel-countries

Before running anything, read the package source — this is a small, low-star package, so the migrations and models in vendor/woodsandwalker/laravel-countries/src are the real documentation:

  • src/Models/Country.php — the model you'll actually use (table name, fillable, casts, scopes)
  • database/migrations/* — column names (iso2 vs iso_2 vs code), indexes, and whether iso2 is unique
  • database/seeders/* — class name and whether it upserts or truncates

Confirm the exact publish tags and seeder class names rather than trusting the README:

php artisan vendor:publish   # interactive list shows available tags/providers

2. Publish and migrate.

php artisan vendor:publish --tag="countries-migrations"
php artisan migrate

3. Seed the reference data. Most country packages ship a seeder you must run explicitly:

php artisan db:seed --class="WoodsAndWalker\LaravelCountries\Database\Seeders\CountrySeeder"

You should end up with ~250 rows. If you get zero, the seeder class name or tag was wrong — dump php artisan db:seed --help or list vendor/.../database/seeders.

4. First real use case. A country dropdown backed by the table, storing an ISO code on your own model:

use WoodsAndWalker\LaravelCountries\Models\Country;

// Controller
$countries = Country::orderBy('name')->pluck('name', 'iso2');

// Request
'country' => ['required', 'string', 'size:2', 'exists:countries,iso2'],

That's the 80% use case: a lookup table you never hand-maintain, plus a stable two-letter code to store on users, addresses, orders, and shipping rules.

Implementation Patterns

Treat the table as immutable reference data. Never let users insert/update countries, and never let business logic depend on the auto-increment id — store iso2 (occasionally iso3) on your own tables. IDs can change if the seeder is rewritten; ISO codes don't.

Foreign key, with a check first. This is the cleanest integrity guarantee, but it requires a unique index on the referenced column:

// confirm the package migration has a unique index on iso2
$table->char('country_code', 2);
$table->foreign('country_code')->references('iso2')->on('countries')
      ->restrictOnDelete()->cascadeOnUpdate();

If iso2 isn't unique in the published migration, add the index in your own follow-up migration, or skip the FK and rely on the exists rule.

Cache the dropdown. A country select on every page render is a wasted query:

// app/Support/Countries.php
public static function options(): array
{
    return Cache::rememberForever('countries.options', fn () =>
        Country::orderBy('name')->pluck('name', 'iso2')->all()
    );
}

Pair this with a small artisan command (countries:clear-cache) that you call after any reseed/deploy.

Validation rule, shared. Wrap the exists check once so casing and table name stay consistent:

class CountryCode implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $valid = is_string($value)
            && Country::where('iso2', strtoupper($value))->exists();

        if (! $valid) {
            $fail('The selected :attribute is not a valid country.');
        }
    }
}

Normalize to uppercase at the boundary (prepareForValidation) and use strtoupper everywhere; MySQL collations are usually case-insensitive but SQLite/Postgres in tests may not be, which produces "works in prod, fails in CI" bugs.

API responses: expose codes, not database IDs.

return CountryResource::make($country);
// { "code": "GB", "code3": "GBR", "name": "United Kingdom", "phone": "+44" }

Consumers (mobile apps, carriers, tax engines) expect ISO 3166-1 alpha-2. Ship that and nothing internal.

Layer your own domain data on top. The package gives you identity (code/name), not policy. Keep your own country_settings table keyed by iso2 for VAT rates, shipping zones, supported-currencies, whether you ship there, etc. Join at query time or hydrate a small DTO. This keeps upgrades of the package safe: you're only ever consuming its identity columns.

Deployment pipeline. Add the seed to your deploy/release step, guarded for production:

php artisan db:seed --class="...\CountrySeeder" --force

If the seeder isn't idempotent (check for truncate() vs upsert()), write your own wrapper that maps the package rows through Country::upsert($rows, ['iso2'], ['name', ...]). Never truncate a table that other tables reference with FKs.

Testing. RefreshDatabase empties the table; seeders don't run automatically:

protected $seed = true; // runs DatabaseSeeder — make sure it calls the country seeder

// or narrowly:
$this->seed(\WoodsAndWalker\LaravelCountries\Database\Seeders\CountrySeeder::class);

If seeding on every test is slow, create a tests/Support/countries.php fixture with the handful of rows your tests need and insert those directly.

Extending the model. If you need scopes (->europe(), ->shippable()), don't edit vendor code — subclass or add a Country model in your app that extends the package model, then bind it in a service provider and update the exists rules to your table. Confirm the package model isn't final first.

Gotchas and Tips

The docs are thin, and the API surface is whatever the source says. With 10 stars, treat the README as a hint. Read the migration before writing FKs, read the seeder before assuming idempotency, and read the model before assuming column names. This takes five minutes and saves an afternoon.

Column naming is the #1 integration trap. iso2, iso_2, code, alpha_2 are all plausible. Grep the migration once and standardize on that name in your app rather than inventing an accessor.

FK to a non-unique column will fail at migrate time on MySQL with a confusing errno: 150. Check for the unique index before adding the constraint — this is the most likely reason a first implementation stalls.

Seeder performance and transactions. ~250 rows can mean ~250 individual insert statements. If the seeder is slow, wrap it: DB::transaction(fn () => $this->seed(...)), or replace it with a chunked upsert.

Do not run it in DatabaseSeeder unconditionally if you also truncate. Order matters: truncate() on your users/addresses tables, then reseed countries, or FKs will block the truncate.

Disputed and non-ISO entities. Expect quirks around Kosovo (XK), Taiwan, and territories like Puerto Rico. If your product needs a specific list (e.g. EU VAT only, or shipping destinations), filter in your own layer rather than forking the seeder — otherwise every package update becomes a merge conflict.

Names are English-only unless proven otherwise. If you need localized country names, check whether the package ships a translations table or JSON column. If not, plan a separate country_translations table keyed by iso2 and populate it from CLDR — don't try to make the package do it.

exists:countries,iso2 is a query per validation. Fine for a single form; in bulk imports (thousands of address rows) load Country::pluck('iso2') into a set once and validate in memory.

Beware the name collision. If your app already has an App\Models\Country, alias the import (use ... as CountryReference;) or you'll silently get the wrong class in a controller — a bug that only shows up when a scope or relationship is missing.

Pinning and upgrades. A 0.x/1.x package at this star count can change column names or seeder behaviour between releases. Pin with a caret constraint you've tested ("^1.0"), read the diff before bumping, and keep the migration published in your repo (which it is, by design) so an upstream change can't silently alter your schema.

License vs data license. The code is MIT, but country datasets often carry attribution requirements (CLDR, ISO, Natural Earth). If you're shipping a commercial product, skim the seeder's data source before assuming MIT covers the data too.

Extension points to prefer: config file (if one is published) for table name and model class; a binding in AppServiceProvider to swap the model; a cached repository/service of your own. Avoid editing anything under vendor/ — the next composer update erases it.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin