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

World Laravel Package

nnjeim/world

Laravel package offering a comprehensive dataset of countries, states, cities, timezones, currencies, languages, and IP geolocation. Access it all through the World facade or ready-made API routes.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require nnjeim/world
php artisan world:install

world:install publishes config/world.php, runs the package migrations and executes WorldSeeder for you. If it refuses to run (the README explicitly tells you to set APP_ENV=local first), fall back to the manual path — it is the same three steps and is what you should script in CI anyway:

php artisan vendor:publish --tag=world --force
php artisan migrate
php artisan db:seed --class=WorldSeeder

Then verify with a single call. The facade is the fastest way to confirm the data landed:

use Nnjeim\World\World;

$action = World::countries(['fields' => 'id,name,iso2']);

if ($action->success) {
    $countries = $action->data; // Illuminate\Support\Collection
}

Every action returns the same envelope: success (bool), message (string), data (Collection), errors (array). Write one helper that unwraps it and you have a consistent contract across all six entities.

First use case: a country/state/city cascading select. World::countries(['fields' => 'id,name,iso2']) for the first dropdown, then World::states(['filters' => ['country_code' => $iso2]]), then World::cities(['filters' => ['state_id' => $id]]). That is 90% of what most teams actually install this package for.

Where to look first: the published config/world.php (route prefix, middleware, enabled modules, localization, DB connection) and the migrations, because the foreign-key shape between world_states, world_cities and world_countries dictates how you write filters. Don't assume it from the README — read the migration.

Implementation Patterns

Facade in a controller, thin and defensive. The action object never throws for "no results" — it returns success: true with an empty collection — so guard on emptiness, not just success:

$action = World::countries([
    'fields'  => 'id,name,iso2,phone_code,currency',
    'filters' => ['region' => 'Europe'],
    'search'  => $request->string('q')->toString(),
]);

return $action->success
    ? response()->json($action->data)
    : response()->json(['errors' => $action->errors], 422);

Inject WorldHelper instead of the facade in services, jobs and anything you want to unit-test. The facade resolves to the same object, but constructor injection lets you swap or mock it.

fields is the projection, filters is the predicate, search is the fuzzy match. fields is a comma-separated string; filters is an array. Relations are requested by name inside fields ('fields' => 'iso2,states,cities'), so the same parameter controls both columns and eager-loads. Always include id in fields if the response feeds another query or a form — the package only returns what you asked for.

Use the API routes as your read layer for SPAs. /api/countries?fields=iso2,cities&filters[phone_code]=44 is a complete, paginated-free endpoint you did not have to write. Point a Select2/Autocomplete at it, and keep the prefix configurable so admin or a versioned api/v1 works without code changes. Disable the routes entirely in config/world.php if the endpoints should not be public.

Drop to Eloquent when you need SQL the DSL cannot express. The models (Nnjeim\World\Models\Country, State, City, …) are ordinary Eloquent models: Country::with('states')->whereIn('iso2', $codes)->get(). This is the right tool for joins, aggregates, whereHas, or exporting — the action layer is a convenience over the models, not a replacement.

Cache the static reference data. Countries, currencies, timezones and languages change on the order of months. Cache::remember('world.countries', now()->addDay(), fn () => World::countries(['fields' => 'id,name,iso2'])->data) removes a surprising amount of query load from every page that renders a country picker. Do not cache cities wholesale — that table is very large; cache per-state if you must.

Geolocate once per session, then store the ID. World::geolocate() returns country.id, state.id, city.id linked to your own tables, so persist country_id on the user/session and use World data for the currency and timezone afterwards. Never call it on every request — see the rate limit in the next section.

$location = World::geolocate(['ip' => $request->ip()]);
$countryId = $location->success ? ($location->data['country']['id'] ?? null) : null;

Derive validation rules from the database. Rule::in(Country::pluck('iso2')) beats a hardcoded 250-entry array that silently rots. Same for currency codes and phone codes.

Gotchas and Tips

  • APP_ENV=local is a hard gate, not a suggestion. The install path expects it. In staging/CI, either set it for the one command or use the three manual steps — and never let a deploy pipeline casually flip the environment variable for the whole app.
  • Re-publish the config on every upgrade. The README is emphatic about this, and --force overwrites config/world.php. Keep your customizations in .env or in a thin config/world-custom.php you merge at boot, or you will lose them on the next composer update.
  • Seeding is heavy and not idempotent by design. Cities alone are in the hundreds of thousands of rows. Run the seeder as a one-off deploy step, not inside every migration run; give it a generous max_execution_time/memory limit, disable the query log, and wrap it in a transaction only if your engine tolerates a transaction that big (MySQL InnoDB with a huge city insert may not). Timezones, currencies and languages are trivial by comparison.
  • search is a leading-wildcard LIKE. It will not use an index on world_cities. On big tables prefer exact filters (country_code, state_id) and reserve search for small, user-typed dropdowns with a minimum length.
  • Filter keys are column names. country_code works on states/cities, country_id works on cities/timezones — mixing them up returns an empty data array rather than an error, which is the classic two-hour debugging session. Log $action->data->count() while wiring filters.
  • The public API routes ship enabled. Check routes.middleware and routes.prefix in the config and add throttle: yourself. An unthrottled /api/cities is an easy accidental DoS on your own database.
  • Geolocation fallback is rate-limited to 45 req/min on the free ip-api.com tier, and it is a third-party dependency in your request path. Cache the result per IP (a day is reasonable), and set WORLD_GEOLOCATE_FALLBACK_API=false in production after downloading the GeoLite2 database with php artisan world:geoip --license=....
  • Proxy header trust is spoofable. The package reads CF-Connecting-IP, X-Forwarded-For, X-Real-IP, CLIENT-IP in order. Behind Cloudflare/nginx that is correct; exposed directly, any client can claim any IP. Configure Laravel's TrustProxies middleware to match your actual topology before relying on World::geolocate() for anything security-adjacent.
  • Geolocate id fields are nullable by contract. If the returned city isn't in your seeded dataset, id is omitted and you get raw geo data only. Always null-coalesce rather than assuming the key exists.
  • WORLD_DB_CONNECTION is great until you join. Putting the large geo tables on a separate read connection keeps your primary database lean, but cross-connection joins with your users table will fail. Keep the ID linkage in your own tables and join in PHP, or keep everything on one connection.
  • Don't confuse the timezone table with timezone handling. It is a lookup of IANA names for display and storage; date math and DST still belong to Carbon/PHP.
  • Currencies have no exchange rates — codes, symbols and names only. If a feature needs conversion, that is a separate data source.
  • Testing tip: stub geolocation with Http::fake() (or disable the fallback in phpunit.xml) so your suite never hits ip-api.com, and seed a small fixture subset rather than the full world dump in test databases.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo