lemonpatwari/bangladeshgeocode
Laravel/PHP package providing Bangladesh geocode data—divisions, districts, upazilas, unions, and villages—with migrations, seeders, models, and helpers for building cascading location dropdowns and address forms.
This package gives you the administrative geography of Bangladesh — Division → District → Upazila → Union — as seedable tables and (typically) Eloquent models, so you don't hand-maintain ~8 divisions, 64 districts, ~495 upazilas and ~4,500 unions.
Minimal path to a working feature:
composer require lemonpatwari/bangladeshgeocode
php artisan vendor:publish --provider="Lemonpatwari\BangladeshGeocode\BangladeshGeocodeServiceProvider"
php artisan migrate
php artisan db:seed --class="BangladeshGeocodeSeeder" # verify the exact class name in the README
Because the package is small and young, confirm the publish tags and seeder class name by reading vendor/lemonpatwari/bangladeshgeocode/src/ and the README.md before you run anything in a shared database — that directory is where you should look first, not the docs. Check in order:
config/ — table name prefixes, whether models are configurable, any enabled switches.database/migrations/ — column names (name, bn_name, division_id, etc.) and whether foreign keys are declared.database/seeders/ — how much data ships and in what format (JSON/PHP array), which tells you how long seeding takes.src/Models/ — relationships (districts(), upazilas(), unions()), and whether coordinates exist.First use case: an address form with four dependent dropdowns. Load divisions server-side, then fetch districts/upazilas/unions via small JSON endpoints as the user selects. That's 90% of what this package is used for, and it validates the whole install in one screen.
Reference the IDs, not the names. Store union_id (or the deepest level you collect) on your addresses/users/orders table and derive division/district/upazila through the relationships. Storing four denormalised name strings means every future spelling correction in the seed data becomes a data-migration for you.
Schema::table('addresses', function (Blueprint $table) {
$table->foreignId('union_id')->nullable()->constrained('unions');
});
Cascading selects via a single controller. Expose one endpoint per level rather than four controllers:
Route::get('/geo/districts/{division}', fn (Division $division) =>
$division->districts()->orderBy('name')->get(['id', 'name'])
);
Keep the payload to id + name. For Select2/Choices.js, add ->pluck('name', 'id') server-side so the front-end does no mapping.
Validate with exists, not custom rules. 'union_id' => ['required', 'integer', 'exists:unions,id'] is enough; the FK constraint is your second line of defence. Don't validate the parent IDs independently — derive them from the selected child to avoid inconsistent tuples (upazila A with union from upazila B).
Cache the top level aggressively, never the leaves. Divisions and districts are effectively static; unions can change. Cache::rememberForever('geo.divisions', ...) for the first dropdown, and query unions live. If you cache unions, key by upazila_id and set a TTL.
Seed once per environment, not per deploy. Put the geo seeder in a dedicated deploy step (php artisan db:seed --class=...) or a isGeoSeeded() guard in DatabaseSeeder, and use RefreshDatabase + the package seeder in feature tests so factories always have valid IDs to pick from.
Reporting. For "orders per division", join through rather than loading models: ->join('unions', ...)->join('upazilas', ...) and group by divisions.name. Model-level withCount chains get slow once you're scanning a large orders table.
Extension point: if you need coordinates or postcodes, add them in your own migration on top of the package's tables rather than editing vendor files — that keeps upgrades clean. If the models are published/configurable, subclass and bind your own in a service provider.
create() in a loop can take minutes. Check whether the seeder uses insert() or upsert(); if not, wrap it in a transaction or chunk it. Locally this is annoying; in CI it's a timeout.composer.lock and treat a bump as a data migration requiring review, not a routine composer update.bn_name column — users search in Bangla. Set the column collation to utf8mb4_unicode_ci.LIKE 'x%'. "Cox's Bazar" and "Chapainawabganj" break intuition. Use LIKE '%term%' on the union level with a LIMIT, or a proper search index.unions → upazilas → districts → divisions), or the reseed fails midway and leaves you with orphans.composer.json ("lemonpatwari/bangladeshgeocode": "^1.0" with a committed lockfile), keep the migrations in version control rather than relying on vendor, and be prepared to fork — it's a small, self-contained dataset, which makes forking cheap.[]). php artisan tinker with Division::count() distinguishes the two in one command.How can I help you explore Laravel packages today?