lwwcas/laravel-countries
Eloquent-first country database for Laravel. Every ISO 3166 Alpha-2, Alpha-3 and numeric code, international phone code, currency, SVG and emoji flag, GeoJSON border and translation is a native Eloquent relationship—one Country:: call away, with no external API needed.
lwwcas/laravel-countries is a reference-data package, not an integration: it ships migrations, an Eloquent Country model, seeders and a dataset (ISO codes, phone codes, currency, flags in three formats, GeoJSON borders, coordinates, population/area/GDP, independence dates, region hierarchy, 9 locales) that is installed into the host application's own database. There is no runtime API call, no external service dependency, and no data egress. That dramatically narrows the evaluation: the decision is not "will it integrate" (it will) but "is this dataset and this maintenance model good enough to become a system of record for country lookups, and can we live with its schema in our database."
Architecture fit. Fits cleanly where the product needs country semantics — localized country/region names, flag rendering, phone-code capture, address/checkout country pickers, map or shipping-zone features. It is a poor fit if the only requirement is a two-column dropdown: it adds two Composer dependencies (spatie/laravel-package-tools, astrotomic/laravel-translatable), a translations table, and a geometry column to solve a problem a static config file already solves. It is also a poor fit where country attributes drive money or compliance logic (pricing tiers by GDP, sanctions lists, tax rules), because the demographic and economic fields are seeded snapshots with no documented refresh contract from a named upstream authority. Treat them as display metadata, never as business inputs.
Structurally it is a terminal dependency — zero packages depend on it, so it cannot create transitive version conflicts upstream; the only coupling risk introduced is astrotomic/laravel-translatable, which shapes how every translation read is written in your codebase (whereTranslation, locale fallbacks). That is a durable architectural commitment, so it deserves explicit sign-off rather than being absorbed as an implementation detail.
Integration feasibility. High. composer require plus php artisan w-countries:install (or w-countries:seed --languages=en,pt when migrations already ran) is a single-pipeline-step adoption. Language scoping at install time is a genuinely thoughtful operational feature — you seed only the locales you serve and can add a market later without a re-seed of everything. Idempotent seeders and --show-commands for manual db:seed equivalence make it CI-friendly and auditable, which is more than most packages in this class offer.
Technical risk. Rated by what would actually hurt in production:
| Risk | Severity | Assessment |
|---|---|---|
| Primary-key stability across re-seeds | High | If we FK users.country_id/orders.country_id to countries.id and a future install/seed rewrites IDs (changelog history already shows country rows removed and duplicates fixed — v3.4.3 removed AN and a duplicate CY), every FK silently points at the wrong country. Must confirm whether IDs are deterministic and whether the seeder truncates or upserts. |
| Staleness of GDP / population / independence / currency | Medium | Fine as display data. Unacceptable as an input to pricing, eligibility, or reporting without an internal refresh job and a disclosure that the number is a snapshot. |
| GeoJSON payload weight | Medium | Polygon data in a text/JSON column means Country::all() or with('translations') on a list endpoint can pull megabytes. Needs a column-selection convention enforced in code review. |
| Single-maintainer bus factor, 132 stars, low visible adoption | Medium | MIT and forkable, and the data is the asset — a fork is viable. But translation accuracy arrives via community PRs (PR #45 Arabic, PR #36 Turkish), so quality is variable and review depth is one person. |
| Version churn and schema migration across majors | Medium | v3 → v4 was a "complete module refactoring"; patch velocity in 4.13.x is high. Budget for reading changelogs on every minor bump, not just majors. |
Table-name collision with an existing countries table |
High (if applicable) | The package owns countries. Any pre-existing homegrown table must be renamed or retired before install, not after. |
| Laravel 13 / PHP 8.4 compatibility claims | Low | CI matrix covers L11/L12/L13; verify against our pinned Laravel and PHP versions and DB driver before committing. |
Key questions to resolve before a build commitment.
countries.id values stable and reproduced identically on re-seed, or is the canonical key iso_alpha2? Which column do we allow as a foreign key?--languages removals delete translation rows destructively? Is that reversible?astrotomic/laravel-translatable already in our dependency tree, and at a compatible constraint?Stack fit. Laravel 11/12/13 (with historical 9/10 support) and PHP 8.x, per the CI matrix and the 4.13.1 PHP 8.4 reliability work. It is a first-class Laravel citizen: service provider, publishable assets, Artisan commands, Eloquent scopes (whereIso, whereIsoAlpha3, whereIsoNumeric, wherePhoneCode, whereGdp, whereLanguage). Fit degrades to zero outside Laravel — no Lumen, no Symfony, no framework-agnostic consumption without effort. Verify our DB driver's JSON/geometry handling and confirm the installer behaves on Postgres as well as MySQL; the docs are Laravel-centric, not driver-centric.
Migration path. Three plausible starting states, each with a different sequence:
country_id or iso_alpha2 columns to domain tables in the same release. Lowest cost, do it early before address/checkout schemas ossify.countries table. Collision risk. Rename to countries_legacy, install the package, build a mapping table on ISO alpha-2, backfill domain FKs, dual-read for one release, then drop legacy. Do not attempt an in-place column-by-column merge.Compatibility. Pin the package version explicitly and treat minor bumps as reviewable changes given the patch velocity. Confirm the astrotomic/laravel-translatable constraint resolves against what we already have. Check that the package's migration batch ordering does not collide with our own migrations (changelog v3.1.0 changed migration ordering once already — a hint that ordering has been fragile).
Sequencing. Recommended four-step delivery:
iso_alpha2 as the stable join key is the defensible default; countries.id only if determinism is proven). Add domain columns, indexes on iso_alpha2.--force in the deploy pipeline after migrate --force; backfill existing rows via mapping table; ship dual-read.Recommendation. Proceed if country data is a genuine product requirement (localized names, flags, maps, phone/currency capture) and the spike confirms ID determinism. Do not proceed if the requirement is a dropdown, or if any business rule will read GDP/population as authoritative.
Maintenance. Low ongoing effort, moderate attention requirement. The table is read-mostly and ~250 rows; there is nothing to tune. The real maintenance load is version and data review: high patch velocity, a v3→v4 refactor precedent, and changelog entries that silently remove country rows or fix duplicates. Assign one owner to read every changelog diff, and pin the version. Data corrections (a renamed capital, an amended GDP figure, a disputed border) arrive as package releases on the maintainer's timetable, not ours — if any country attribute becomes customer-facing or contractual, we need our own override table rather than waiting upstream.
Support. Community-only: GitHub issues, a documentation site, sponsor/star-driven engagement. No commercial support, no SLA, no CVE process documented. Mitigations are cheap and should be taken regardless: vendor the exact version, keep a fork path exercised (the data is MIT and self-contained, so a fork is a realistic fallback), and record the maintainer contact in the dependency register. The 132-star, single-maintainer profile is the main unsupported-asset risk, and it is a continuity risk rather than a functionality risk.
Scaling. Negligible on the query side — this is dictionary data and should be cached in-process or in the app cache after first read, with a version key bumped on deploy. Two realistic scaling hazards: (1) GeoJSON, if any list endpoint selects * the response size and PHP memory footprint jump by orders of magnitude; enforce explicit select() and a dedicated geometry accessor. (2) Translation joins — N+1 queries will appear the moment country names are rendered in a table; mandate eager loading and a cache-friendly lookup repository. Neither is a capacity problem; both are code-review problems.
Failure modes. Ranked by likelihood of causing a bad deploy:
countries → country pickers silently empty. Mitigate with a post-deploy assertion (row count > 0, expected locales present) as a health check.country_id values now resolve to the wrong nation. Highest-severity, lowest-visibility failure — wrong country on an invoice or a shipping label. Mitigate with the alpha-2 join key and a reconciliation job.countries table causes the installer to fail or, worse, to apply against the wrong schema. Mitigate with a pre-deploy schema check.Ramp-up. Low for anyone who already writes Eloquent — the mental model is "a table you have, with a model you query," and the documentation site includes a full method listing. Realistic ramp: a competent Laravel developer is productive in half a day; the week of calendar time is spent on our conventions, not the package. Those conventions must be written down and reviewed: which ISO column is canonical, when to cache, never select * on endpoints that include geometry, never derive business logic from demographic fields, and never trust countries.id unless the spike proved it. The failure mode of an easy-to-adopt package is unowned ad-hoc usage, so a short internal standard is more valuable here than any training session.
How can I help you explore Laravel packages today?