elaborate-code/laravel-algerian-states
Laravel package providing Algeria's wilayas (states) with official codes and names, ready to seed and use in address forms, validation rules, and dropdowns. Simple installation, publishable migrations, and a clean API for querying states in any Laravel app.
Install and confirm the data layer
composer require elaborate-code/laravel-algerian-provinces:^2.0
v2.0.0 pulls in calebporzio/sushi, which means the province (wilaya) dataset ships inside the package as a static array and is exposed through an Eloquent model backed by an in-memory SQLite database. There are no migrations to publish, no seeders to run, and no php artisan db:seed step — after composer require the data is queryable immediately.
Where to look first (in this order)
vendor/elaborate-code/laravel-algerian-provinces/src/Models/ — the model classes and their $rows / data source. This is the authoritative schema: column names, whether names are Arabic/French/both, and whether IDs or codes are the stable key.config/ inside the package — check whether anything is publishable (table name, locale column preference, Sushi cache path).First use case — a wilaya dropdown in a form
use ElaborateCode\AlgerianProvinces\Models\Province;
$provinces = Province::query()->orderBy('code')->get();
<select name="province_id">
@foreach ($provinces as $province)
<option value="{{ $province->id }}">{{ $province->name }}</option>
@endforeach
</select>
That single query is the 80% use case: address forms, delivery zones, dealer/agency registration, shipping-rate tables keyed by wilaya. Everything else in this package is a variation on "give me the reference list, and let me filter the child list by parent."
Store the foreign key on your own table, resolve with belongsTo
Because the package model lives on its own (Sushi) connection, the correct integration is a plain integer column on your model plus a normal relationship:
Schema::table('orders', fn (Blueprint $t) => $t->foreignId('province_id')->nullable()->index());
public function province(): BelongsTo
{
return $this->belongsTo(Province::class);
}
belongsTo / hasMany work fine — they are separate queries against the model's own connection. What does not work is a SQL JOIN across your app tables and the province table, or DB::table('provinces') on the default connection. Keep the province data out of raw SQL.
Dependent dropdowns (province → commune/daira)
Model the cascade as two endpoints returning JSON, not as a single eager-loaded page — the commune list is large and slow to render inline:
Route::get('/api/provinces/{province}/communes', fn (Province $province) =>
$province->communes()->orderBy('name')->get(['id', 'name'])
);
If the package does not declare the inverse relation, add it in a local subclass or resolve it with Commune::where('province_id', $province->id)->get().
Cache the reference lists
Sushi builds its in-memory database lazily on the first query per process. On a page that renders a wilaya list on every request, wrap it:
$provinces = Cache::rememberForever('ref:provinces', fn () => Province::orderBy('code')->get());
Treat this as effectively static reference data — rememberForever is appropriate, with an explicit Cache::forget('ref:provinces') in a deploy/upgrade hook so a package data update actually lands.
Validate against the dataset in PHP, not SQL
The exists:provinces,id rule points at your default connection and will fail. Either qualify the connection or validate in-memory:
'province_id' => ['required', Rule::in(Province::query()->pluck('id'))],
Rule::in with a plucked collection is correct, cheap (cached above), and connection-agnostic.
Localisation accessor
Algerian wilayas have Arabic and French/Latin names. Rather than branching in every view, add a single accessor on a thin subclass:
class Province extends \ElaborateCode\AlgerianProvinces\Models\Province
{
public function getLocalizedNameAttribute(): string
{
return match (app()->getLocale()) {
'ar' => $this->name_ar ?? $this->name,
default => $this->name_fr ?? $this->name,
};
}
}
Swap the subclass in via a container binding or by referencing your class directly in app code — this keeps the upgrade surface to one file.
Seed your own fixtures from stable codes, not auto-increment IDs
The wilaya numbering (1–58) is nationally defined and stable; auto-increment IDs are an implementation detail. In factories and seeders, resolve by code:
'province_id' => Province::where('code', 16)->value('id'),
This survives a package data reshuffle that would silently renumber IDs.
Sushi models are read-only. Province::create(), ->update(), and ->delete() will appear to succeed and then vanish — the SQLite database is in-memory and rebuilt on the next process boot. If your application needs editable wilayas (custom shipping surcharges, per-wilaya settings, enable/disable), create your own provinces table with a code column and keep the package as a read-only reference source. Do not discover this in production after an admin edits a row.
v1 → v2 is a major bump; assume breakage. The only documented change is the Sushi dependency, but a 1.0.0 → 2.0.0 transition is exactly where a migration-plus-seeder package becomes a data-in-package one. On upgrade: remove any migrations/seeders you published from v1 (they will now duplicate or conflict with the Sushi source), re-check the model namespace, and grep your codebase for anything that assumed a real database table (DB::table('provinces'), exists: rules, withCount joins, php artisan migrate:fresh followed by seeded data). Also clear cached config and OPcache as part of the deploy — class-not-found errors after a major bump are almost always a stale autoloader or OPcache.
Sushi needs a writable cache/temp location. Depending on the Sushi version, schema caching may write under storage/framework/cache. In containerised or read-only-filesystem deploys, a non-writable storage/ will surface as a confusing "unable to open database file" or SQLite error rather than anything mentioning Sushi. Check storage/framework/cache permissions first when the package works locally but not in CI/production.
In-memory cost is per-process, not per-query. Under PHP-FPM the SQLite database is rebuilt on every request that touches the model; under Octane or a queue worker it is built once and reused. That asymmetry makes performance benchmarks misleading — measure in the mode you actually deploy.
Arabic sorting will be wrong if you delegate it to SQLite. The bundled SQLite has no ICU collation, so orderBy('name_ar') sorts by code point, which is not alphabetical Arabic. Sort in PHP with Collator (collator_sort($names, Collator::SORT_STRING)) or order by code and present in wilaya-number order, which is what Algerian users generally expect anyway.
Confirm the dataset version before relying on it. Algeria moved from 48 to 58 wilayas (2019) and added new daïras (2021). Verify the package actually contains 58 provinces and check whether commune coverage is complete, because partial commune data is the most common complaint about packages in this category — and it manifests as a silently missing dropdown option, not an error.
Pin the major version. With a data-carrying package, a minor release can change names, codes, or row counts. Use ^2.0 and review the diff on bumps; if you subclass the models or rely on specific column names, add a small test asserting the row count and a couple of known codes so a data regression fails your suite instead of your checkout form.
How can I help you explore Laravel packages today?