fsi/teryt-database-bundle
Symfony bundle for integrating Poland’s TERYT territorial register. Provides entities, import commands, and query helpers for voivodeships, counties, municipalities, localities, streets, and postal data, simplifying address and location validation in PHP applications.
This is a Symfony bundle, not a Laravel package. It is built around Symfony DI, Doctrine ORM, and console commands, so do not expect php artisan package:discover, publishable migrations, or Eloquent models. Treat it as either a reference implementation for TERYT data or as a sidecar import tool.
First steps:
src/Entity/ for the TERYT schemasrc/Command/ for import commandsResources/config/ for service wiring and parameterscomposer require fsi/teryt-database-bundle
# configure a Symfony console app or existing Symfony app with DB credentials
php bin/console list teryt
php bin/console teryt:import # exact command name depends on the bundle version
First Laravel use case: an address picker backed by TERYT hierarchy — voivodeship → county → municipality → locality → street. Store TERYT codes on your addresses table, not display names, and hydrate selects from the imported reference tables.
Keep TERYT data read-only in Laravel. Map the bundle tables with lightweight Eloquent models:
class TerytVoivodeship extends Model
{
protected $table = 'teryt_voivodeships'; // verify actual table name
protected $primaryKey = 'id';
public $timestamps = false;
protected $guarded = [];
}
Use string primary keys/codes, disable timestamps, and avoid assuming Laravel-style pluralized table names. Inspect the Doctrine mappings first.
Import on a schedule, not in a request. Run the Symfony importer from a Laravel scheduled command or queued job:
$schedule->exec('php /path/to/symfony/bin/console teryt:import')
->weekly()
->withoutOverlapping()
->runInBackground();
For large TERYT releases, import into staging tables and swap/upsert in a transaction. Keep a teryt_version or imported_at column so you can trace which dataset an address was validated against.
Build hierarchical API endpoints. Example:
Route::get('/teryt/counties', function (Request $request) {
return TerytCounty::where('voivodeship_id', $request->string('voivodeship_id'))
->orderBy('name')
->get(['id', 'name']);
});
Cache voivodeships and counties aggressively; localities and streets are larger and may need search endpoints with prefix matching.
Validate parent-child consistency. A common bug is accepting a municipality ID that does not belong to the selected county. Add a custom validation rule:
Validator::extend('teryt_child_of', function ($attribute, $value, $parameters) {
return TerytMunicipality::whereKey($value)
->where('county_id', request($parameters[0]))
->exists();
});
Store codes, denormalize names selectively. Persist TERYT codes for joins and validation. Store a snapshot of the selected name on orders/addresses if historical display matters, because TERYT names and boundaries change.
If porting to Laravel: use the bundle entities as your schema reference, then write Laravel migrations and a custom importer. Use Http::get() or curl for downloads, XMLReader for large XML files, chunked inserts, and insertOrIgnore/upsert. Do not load the entire TERYT dataset into memory.
Expose search carefully. Street names are the hardest TERYT layer: many localities, diacritics, and duplicate names. Use indexed LIKE 'prefix%' queries, normalize case, and always require a locality context.
vendor:publish, no Eloquent, and no Laravel migrations. Any tutorial implying php artisan support is wrong.int; use CHAR/VARCHAR and string casts in Eloquent.voivodeship_id.-vvv, inspect Doctrine SQL logs, and verify the TERYT source URL still points to the current official dataset. Government download URLs and file formats change.How can I help you explore Laravel packages today?