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.
Architecture fit. This is not a PHP library — it is a data artifact (JSON/YAML/CSV/XML flat files) with a thin Packagist wrapper. There is no service provider, no config contract, no facade, no exception hierarchy, no API surface to design against. Architecturally it fits in exactly one place: as a build-time seed source for a normalized countries lookup table (plus country_translations, currencies, optionally subdivisions), consumed behind a first-party CountryRepository interface. It does not fit as a runtime dependency you inject, mock, or version-pin at the application layer. If a team treats it as a live dependency (reading vendor/mledoze/countries/dist/countries.json at request time or in a scheduled job), they have created an undeclared coupling to a vendor's internal file layout that upstream has never committed to stabilizing.
Integration feasibility. High mechanically, low strategically. Ingest is a one-to-two-day job: an artisan command that reads the JSON, validates the expected keys, and upserts into Eloquent models inside a transaction. The data volume (~250 country records, a few thousand subdivisions, a few hundred translations) is small enough to seed synchronously in a deploy hook and cache entirely in memory or Redis. There are no framework or PHP-version constraints to reconcile, which is genuinely the package's strongest technical property — it cannot conflict with anything. The friction is that "integration" here means we write the integration, and we own it forever.
Technical risk. Three items, ordered by severity:
Key questions for the team before proceeding:
league/iso3166 (MIT) and symfony/intl (MIT), which together cover ISO 3166-1 codes, English names, and ICU-localized display names behind a maintained PHP API? What does mledoze/countries give us that those do not — subdivisions, demonyms, lat/lng, border adjacency, TLD?The opportunity score of 4.89 reflects reality: this is a commodity dataset with near-zero differentiation and a licensing drag. The correct framing is "buy vs. build vs. borrow a seed file," not "adopt a package."
Stack fit. Laravel 10/11 on PHP 8.1+ — fully compatible, because there is nothing to be incompatible with. Target design:
countries table: id, iso2 (unique, indexed), iso3, numeric_code, name_common, name_official, region, subregion, capital, currency_codes (JSON), calling_code, tld, lat, lng, area_km2, independent (bool).country_translations table keyed on (country_id, locale).CountryRepository contract bound in a CountryServiceProvider, backed by a cache-aside read (Cache::rememberForever with a version key bumped on deploy).CountryCode validation rule delegating to the repository, plus a Blade/Blade-component select so forms stop hand-rolling country dropdowns.This is the pattern that makes the underlying source swappable. If legal kills ODbL, we re-point the importer at a different dataset and change nothing else — that optionality is the whole design goal.
Migration path (phased, legal-gated):
Compatibility. No PHP version constraints, no Laravel constraints, no transitive dependency graph, no autoloader conflicts. If we vendor only the JSON, the dependency graph impact is literally zero. If we keep the composer package, we inherit its autoload map and nothing else.
Sequencing. Legal → schema → importer → repository + cache → validation rule + UI component → update pipeline. Roughly 1.5 sprints for one mid-level engineer, with the entire schedule contingent on Phase 0. Do not run Phase 0 in parallel with Phase 1; the cost of a false start here is a full teardown of seeded reference data and every foreign key pointing at it.
Fallback plan. If legal does not clear ODbL, the MIT path is league/iso3166 for codes and names plus symfony/intl for localized display names. Coverage is narrower (no subdivisions, demonyms, or border adjacency), so confirm the product actually needs those fields before assuming the fallback is a downgrade.
Maintenance. The cost shifts entirely to us after import. Upstream shows recent activity (last release Feb 2025) but no disciplined changelog or data-shape versioning — updates must be reviewed as raw diffs. Realistic budget: ~1 engineer-day per quarter for diff review plus a short product/compliance pass on any politically sensitive naming change. That is the recurring cost, and it is small but permanent.
Support. No vendor, no SLA, no support channel beyond GitHub issues. Acceptable for a commodity reference dataset — but it means there is no one to escalate to when a country rename breaks an address form on a Friday.
Scaling. Non-issue. ~250 rows, seeded once, cached in Redis or a static array. Reads are sub-millisecond and effectively free. This package will never appear on a capacity or latency dashboard. Do not let a scaling concern — real or imagined — enter the decision.
Failure modes, ranked:
source_version column on the table so we can prove what snapshot is live.Ramp-up. Engineering ramp is roughly half a day — it is JSON in a file, and any Laravel developer can seed it. Infrastructure ramp is zero. The real ramp-up is legal and product-organizational, and that is where a TPM should spend their attention: getting counsel to render an ODbL opinion and getting product to ratify a territory-representation policy are the two long-lead items, and both sit outside the engineering critical path in a way that reliably makes them the actual schedule risk.
Recommendation. Adopt as a one-time seed source with the dependency removed after import, contingent on ODbL clearance. Do not adopt as a maintained runtime dependency. If legal cannot clear ODbL within two weeks, default to the MIT path and stop spending cycles on this evaluation — the dataset is a commodity, and the differentiation it offers is not worth an open licensing question in a commercial product.
How can I help you explore Laravel packages today?