artesaos/cidadesbr
Laravel package that ships a ready-to-use database of Brazilian states and cities, complete with migrations, seeders, Eloquent models and relationships, plus query helpers for filtering cities by state—ideal for address forms and select inputs.
Verify compatibility before anything else. This package's last release was in 2015, which means it targets the Laravel 4.x/5.0–5.1 era (service providers registered in config/app.php, no package auto-discovery, illuminate/support pinned to old versions). Open vendor/artesaos/cidadesbr/composer.json right after install and read the require block — that tells you more about whether it will run on your Laravel version than any README will.
composer require artesaos/cidadesbr
If you're on Laravel 5.5+, check whether the provider is auto-discovered. If not, register it manually in config/app.php:
'providers' => [
// ...
Artesaos\CidadesBr\CidadesBrServiceProvider::class,
],
Publish and inspect. Rather than guessing table names and seeder class names, publish the package assets and read them:
php artisan vendor:publish --provider="Artesaos\CidadesBr\CidadesBrServiceProvider"
Then look at database/migrations/ and database/seeds/ inside the published/vendor path. You want to answer three questions before writing any code:
estados and cidades.)Artesaos\CidadesBr\Models\Estado and ...\Cidade — confirm in src/.)db:seed --class=...?Run the migration and seed once. The municipality table is large (~5,570 rows in the 2015 IBGE snapshot), so seed as a deliberate, one-off step, not as part of every migrate:
php artisan migrate
php artisan db:seed --class=CidadesBrSeeder # confirm exact class name
First use case: a two-level address form. This is what 90% of consumers of this package use it for — a <select> of states that drives a <select> of cities. Get the state list once, then fetch cities per state:
use Artesaos\CidadesBr\Models\Estado;
// In a controller / view composer
$estados = Estado::orderBy('sigla')->get(['id', 'sigla', 'nome']);
That's the whole getting-started path: publish, migrate, seed, then Estado::... and Cidade::... in your form controller.
Don't render all 5,570 cities into the page. Ship states server-side and load cities over a small JSON endpoint:
// routes/web.php
Route::get('/api/estados/{estado}/cidades', function (Estado $estado) {
return $estado->cidades()->orderBy('nome')->get(['id', 'nome']);
});
document.querySelector('#estado').addEventListener('change', async (e) => {
const res = await fetch(`/api/estados/${e.target.value}/cidades`);
const cidades = await res.json();
const select = document.querySelector('#cidade');
select.innerHTML = cidades
.map(c => `<option value="${c.id}">${c.nome}</option>`)
.join('');
});
Because the tables exist in your own database, you get free referential validation. Always validate the pair together — a valid cidade_id from the wrong state is a data-integrity bug:
$request->validate([
'estado_id' => ['required', 'integer', 'exists:estados,id'],
'cidade_id' => [
'required', 'integer',
Rule::exists('cidades', 'id')->where('estado_id', $request->estado_id),
],
]);
Store cidade_id as the canonical key. But also persist the estado_id/sigla on the address record — it makes filtering, shipping-zone rules, and reporting far cheaper than joining back through cidades → estados on every query:
$address->fill([
'cidade_id' => $request->cidade_id,
'estado_id' => $request->estado_id,
'uf' => Estado::find($request->estado_id)->sigla,
]);
The package ships the relationships, so use them rather than re-querying:
$orders = Order::with('cidade.estado')->paginate(50);
If the relation names differ from cidade/estado, map them once in your own models and move on.
The cleanest long-term pattern: never reference Artesaos\CidadesBr\Models\* directly in application code. Subclass them (or alias them) so you can add scopes, casts, and swap the data source later without touching call sites:
namespace App\Models;
use Artesaos\CidadesBr\Models\Cidade as BaseCidade;
class Cidade extends BaseCidade
{
public function scopeSearch($query, string $term)
{
return $query->where('nome', 'like', "%{$term}%");
}
}
Then configure the package (if it exposes a config for model classes) or simply use App\Models\Cidade everywhere. This is your migration path off the package when it becomes a liability.
States are static; query them once, not per request:
$estados = Cache::rememberForever('estados', fn () =>
Estado::orderBy('sigla')->get(['id', 'sigla', 'nome'])
);
Cache cities per state ("cidades.{$estadoId}") if your form endpoint is hot.
Seeding ~5,570 rows on every RefreshDatabase test run is wasteful. Seeder classes are just classes — call the specific one only in the test suite that needs it, or seed a small fixture subset in setUp():
protected function setUp(): void
{
parent::setUp();
$this->seed(\Artesaos\CidadesBr\Seeders\CidadesSeeder::class); // exact class from the package
}
The package has no coordinates and no CEP ranges. If you need geo or postal features, publish the migration and add columns in a new migration (ALTER TABLE cidades ADD lat ...), then backfill from the IBGE API. Don't edit the published migration in place — you'll lose the change on the next publish.
It is effectively unmaintained. One release in 2015 means no Laravel 8/9/10/11 support, no auto-discovery, and potential deprecation warnings on PHP 8.x. Treat adoption as "vendor this code" rather than "install and forget." A realistic fork plan: copy the migration + the raw IBGE CSV/seed data into your own database/ folder and delete the dependency. You keep the data, you own the upgrade path.
The data is a 2015 IBGE snapshot. Municipalities created after that date are missing, and any IBGE corrections since then aren't there. For most address forms this is acceptable, but if you need completeness, re-seed from the current IBGE API (servicodados.ibge.gov.br/api/v1/localidades/municipios) instead of relying on the bundled seed.
Seeding is slow and not obviously idempotent. Re-running db:seed --class=... may duplicate rows unless the seeder uses updateOrCreate or truncates first. Check the seeder source; if it does plain inserts, guard with if (Estado::count() === 0) in your own wrapper command.
Migration collisions. Published migrations get the timestamp of the publish moment, which can place them out of order relative to your existing tables. If cidades has a foreign key to estados, ensure the estados migration sorts first — otherwise rename the published files.
Accents and collation. City names like "São Paulo" and "Araçatuba" depend on utf8mb4 and a case/accent-insensitive collation for LIKE searches. If your cidades.nome column uses utf8mb4_bin, searching "sao paulo" won't match "São Paulo" — normalize on write (strip diacritics into a nome_normalizado column) if you need forgiving search.
Don't dump the whole table to the frontend. No Cidade::all() in a Blade loop or a JSON payload. Use per-state endpoints and paginate any administrative screens.
Foreign-key deletion order. On teardown/truncation (tests using RefreshDatabase, migrate:fresh), drop cidades before estados if FKs are enforced; otherwise disable FK checks around bulk seed/truncate operations.
Model conventions may surprise you. Old packages often omit $fillable/$guarded, use nonstandard $table/$primaryKey, or don't use timestamps. Read the two model files — it's five minutes and saves a debugging session.
Extension points to rely on: the published migrations (add your own columns via a follow-up migration), the seeder classes (call them selectively from your own seeder/command), and subclassing the models (add scopes, casts, accessors). Config, if present, is thin — don't expect a pluggable data source.
Have an exit strategy. The value here is the data, not the code. Keep city/state references behind your own App\Models wrappers and a repository/cache layer so that swapping in a maintained dataset — or the live IBGE API — is a contained change rather than a hunt through every form and query.
How can I help you explore Laravel packages today?