mledoze/countries
PHP library of world country data: names, ISO codes, capitals, regions, currencies, languages, borders, area, population, and translations. Ships as JSON, CSV, XML, YAML, and PHP arrays for easy import.
mledoze/countries is a data package, not a Laravel package. It ships country records as JSON/YAML/CSV/XML; there is no service provider, model, or validation rule. Your first job is to load vendor/mledoze/countries/dist/countries.json and decide whether to read it at runtime or seed it into a table.
Install and confirm where the data landed:
composer require mledoze/countries
ls vendor/mledoze/countries/dist
# countries.json, countries.yaml, countries.csv, countries.xml
Inspect one record before writing any schema — every field you rely on should be verified against a real row:
$countries = json_decode(
file_get_contents(base_path('vendor/mledoze/countries/dist/countries.json')),
true,
flags: JSON_THROW_ON_ERROR
);
dump(count($countries)); // ~250 entries
dump(array_keys($countries[0])); // cca2, cca3, name, currencies, idd, ...
A good first use case is a country select on an address form: cca2 as the option value, name.common as the label, and flag/currencies/idd as adjacent form defaults. Build that end-to-end before seeding a database; it forces you to confront translation fallbacks, calling codes, and territories early.
Wrap loading in a cached singleton. Never decode the ~2.5 MB JSON on each request.
final class CountryRepository
{
public function all(): array
{
return Cache::rememberForever('countries.data', function () {
return json_decode(
file_get_contents(base_path('vendor/mledoze/countries/dist/countries.json')),
true, 512, JSON_THROW_ON_ERROR
);
});
}
public function find(string $cca2): ?array
{
return collect($this->all())->firstWhere('cca2', strtoupper($cca2));
}
}
Bind it as a singleton in a service provider and key a keyBy('cca2') index inside it to avoid repeated firstWhere scans in loops.
Seed to Eloquent when you need joins, filtering, or admin editing. Create a countries table with cca2 (unique), cca3 (unique), name_common, name_official, region, subregion, currency_code, calling_code, flag_emoji, latitude, longitude, un_member, and a raw JSON column for the untouched record. Use upsert on cca2 so reruns are idempotent:
Country::upsert($rows, ['cca2'], ['name_common', 'region', 'raw']);
Keep the raw column — you will want fields later without re-migrating.
Validation rule. Wrap the data set in a custom rule rather than Rule::in(...) with a hardcoded array:
final class CountryCode implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! app(CountryRepository::class)->find((string) $value)) {
$fail("The {$attribute} is not a valid country code.");
}
}
}
Use cca2 for forms (ISO 3166-1 alpha-2, matches most payment/shipping APIs); store cca3 alongside it for internal joins and analytics.
Localization. Use translations[$locale] with a fallback to eng:
$label = $country['translations'][app()->getLocale()]['common']
?? $country['translations']['eng']['common']
?? $country['name']['common'];
Note the keys are ISO 639-3 (fra, deu, zho), not Laravel's usual fr/de/zh — maintain an explicit map.
Domain helpers. Derive defaults from the record: idd.root . $idd.suffixes[0] for phone fields (with caveats below), array_key_first($currencies) for billing currency, $timezones[0] for user timezone, region/subregion for shipping zones and EU/EEA checks. For forms that only need a list, expose a small API endpoint (GET /api/countries?region=Europe) backed by the cached repository so the frontend doesn't ship the full JSON.
Flags. flags.svg/flags.png are remote URLs; flag is an emoji (regional indicator pair). For production UI, self-host SVGs or use the emoji and accept OS font differences. Don't hotlink in high-traffic pages.
unMember is false for many entries, independent can be null, and Kosovo uses cca2: "XK" with status: "user-assigned". Filter explicitly if your product only serves sovereign states.idd.root + idd.suffixes can contain multiple suffixes (NANP territories share +1), and idd may be empty. Don't blindly concatenate; build a small resolver and test against US, GB, PR, and AQ.currencies is { "USD": { "name": "...", "symbol": "$" } }, can contain multiple currencies, and is empty for Antarctica. Treat "no currency" as valid.capital can be [] and capitalInfo.latlng may be missing; borders is [] for islands. Guard before indexing.name.common for UI, name.official for legal documents. Some official names are long and include "Republic of…"; don't use them in dropdowns.utf8mb4 columns and JSON_UNESCAPED_UNICODE if writing files.Illuminate\Support\Facades\File + Symfony\Component\Yaml\Yaml::parseFile() is fine but slower than JSON.cca2 rows so an update fails loudly instead of silently changing dropdowns.Cache::forget('countries.data') in a deploy hook, otherwise a package bump won't reach production.startOfWeek, car.side, postalCode.regex) are available without re-importing; add a config array listing which fields your API exposes so you can trim payloads later.How can I help you explore Laravel packages today?