plin-code/laravel-istat-geography

Laravel package for importing and managing Italian geography data from ISTAT

Maintainers

Package info

github.com/plin-code/laravel-istat-geography

pkg:composer/plin-code/laravel-istat-geography

Fund package maintenance!

PlinCode

Statistics

Installs: 5 006

Dependents: 0

Suggesters: 0

Stars: 8

Open Issues: 0

v1.2.0 2026-03-19 13:49 UTC

This package is not auto-updated.

Last update: 2026-03-19 14:02:06 UTC


README

Laravel ISTAT Geography

A Laravel package for importing and managing Italian geographical data from ISTAT.

Features

  • ๐Ÿ‡ฎ๐Ÿ‡น Import Italian regions, provinces, and municipalities from ISTAT
  • ๐Ÿ“ฎ Import Italian postal codes (CAP) with support for multi-CAP municipalities
  • ๐Ÿ”„ Incremental updates: add new records, update changes, soft-delete removed ones
  • ๐Ÿ“Š Daily CSV caching to avoid unnecessary requests
  • ๐Ÿ”— Eloquent models with hierarchical relationships
  • โšก Artisan commands for easy data import and synchronization
  • ๐Ÿ”ง Fully configurable via configuration file
  • ๐Ÿ†” UUID primary keys and soft deletes support
  • ๐Ÿงช Comprehensive test suite with mocked HTTP requests

Requirements

  • PHP 8.3+
  • Laravel 11.0+ or 12.0+
  • league/csv 9.0+
  • guzzlehttp/guzzle 7.0+

Installation

composer require plin-code/laravel-istat-geography

Quick Start

  1. Install the package:
composer require plin-code/laravel-istat-geography
  1. Publish the configuration:
php artisan vendor:publish --provider="PlinCode\IstatGeography\IstatGeographyServiceProvider"
  1. Run migrations:
php artisan migrate
  1. Import the data:
php artisan geography:import

That's it! You now have all Italian geographical data in your database.

Commands

geography:import

Performs a full import of all geographical data from ISTAT. Use this for the initial data load.

php artisan geography:import

Options

Option Description
--cap Also import postal codes (CAP) after ISTAT data
--cap-only Import only postal codes, skip ISTAT data (requires existing municipalities)
--cap-file=<path> Use a local JSON file for CAP data instead of downloading

Examples

# Import ISTAT data only
php artisan geography:import

# Import ISTAT data + CAP (using local file - recommended)
php artisan geography:import --cap --cap-file=cap-dataset.json

# Update only CAP on existing municipalities
php artisan geography:import --cap-only --cap-file=cap-dataset.json

Note: The remote GeoJSON with geometries is ~464MB. Using --cap-file with a preprocessed JSON file (~3MB) is recommended for better performance.

geography:download-cap

Downloads CAP GeoJSON data and saves it locally for offline import. Useful when you want to download once and import multiple times.

# Download from default URL (config/env)
php artisan geography:download-cap

# Download from custom URL
php artisan geography:download-cap --url=https://example.com/cap.json

# Specify output path
php artisan geography:download-cap --output=storage/app/my-cap.json

After downloading, import with:

php artisan geography:import --cap --cap-file=storage/app/cap-dataset.json

geography:update

Incrementally synchronizes your database with the latest ISTAT data. It compares the current ISTAT CSV against your existing records and applies only the differences: new records are added, changed records are updated, and records no longer present in ISTAT are soft-deleted.

php artisan geography:update

Options

Option Description
--dry-run Simulate the update without making any database changes. Shows what would be added, modified, or deleted.
--force Continue execution even if non-critical errors occur (errors are logged as warnings).

Verbosity Levels

Flag Output
(none) Final summary only (e.g. 3 added, 1 modified, 0 deleted)
-v Download progress, list of new/modified/suppressed records, progress bar
-vv Field-level change details (e.g. name: Old Name โ†’ New Name)
-vvv Debug output with timing information for each operation

Examples

# Preview changes without applying them
php artisan geography:update --dry-run

# Run with verbose output
php artisan geography:update -v

# Run with full debug output
php artisan geography:update -vvv

# Force continue on non-critical errors
php artisan geography:update --force

All database operations are wrapped in a transaction. If any error occurs (and --force is not set), all changes are automatically rolled back.

Configuration

Publish the configuration file:

php artisan vendor:publish --provider="PlinCode\IstatGeography\IstatGeographyServiceProvider"

The config/istat-geography.php file allows you to customize:

  • Table names: Customize the database table names
  • Model classes: Use your own model classes by extending the base ones
  • CSV URL: Change the ISTAT data source URL (also via ISTAT_CSV_URL env)
  • CAP GeoJSON URL: Change the CAP data source URL (also via CAP_GEOJSON_URL env)
  • Temporary file name: Customize the cache file name

Example Configuration

return [
    'tables' => [
        'regions' => 'my_regions',
        'provinces' => 'my_provinces',
        'municipalities' => 'my_municipalities',
    ],
    'models' => [
        'region' => \App\Models\Region::class,
        'province' => \App\Models\Province::class,
        'municipality' => \App\Models\Municipality::class,
    ],
    'import' => [
        'csv_url' => 'https://custom-url.com/data.csv',
        'temp_filename' => 'my_istat_data.csv',
    ],
];

Models

The package provides three Eloquent models:

Region

use PlinCode\IstatGeography\Models\Geography\Region;

$region = Region::where('name', 'Piemonte')->first();
$provinces = $region->provinces;

Province

use PlinCode\IstatGeography\Models\Geography\Province;

$province = Province::where('code', 'TO')->first();
$municipalities = $province->municipalities;
$region = $province->region;

Municipality

use PlinCode\IstatGeography\Models\Geography\Municipality;

$municipality = Municipality::where('name', 'Torino')->first();
$province = $municipality->province;

ISTAT Fields

Each model exposes a static istatFields() method that returns the list of fields managed by ISTAT data. These are the fields that the geography:update command is allowed to overwrite. Any additional fields you add to your extended models will not be touched during updates.

Region::istatFields();       // ['name', 'istat_code']
Province::istatFields();     // ['name', 'code', 'istat_code', 'region_id']
Municipality::istatFields(); // ['name', 'istat_code', 'province_id', 'bel_code']
Municipality::capFields();   // ['postal_code', 'postal_codes']

Extending Models

If you want to use the package models in your main project, you can extend them:

// app/Models/Region.php
namespace App\Models;

use PlinCode\IstatGeography\Models\Geography\Region as BaseRegion;

class Region extends BaseRegion
{
    // Add your project-specific logic here
    public function customMethod()
    {
        return $this->provinces()->count();
    }
}
// app/Models/Province.php
namespace App\Models;

use PlinCode\IstatGeography\Models\Geography\Province as BaseProvince;

class Province extends BaseProvince
{
    // Add your project-specific logic here
}
// app/Models/Municipality.php
namespace App\Models;

use PlinCode\IstatGeography\Models\Geography\Municipality as BaseMunicipality;

class Municipality extends BaseMunicipality
{
    // Add your project-specific logic here
}

Remember to update the models section in the configuration file to point to your custom classes.

Database Structure

Regions

  • id (UUID, primary key)
  • name (string)
  • istat_code (string, unique)
  • created_at, updated_at, deleted_at

Provinces

  • id (UUID, primary key)
  • region_id (UUID, foreign key)
  • name (string)
  • code (string, unique)
  • istat_code (string, unique)
  • created_at, updated_at, deleted_at

Municipalities

  • id (UUID, primary key)
  • province_id (UUID, foreign key)
  • name (string)
  • istat_code (string, unique)
  • bel_code (string, nullable) - Cadastral/Belfiore code for CAP matching
  • postal_code (string, nullable) - Primary postal code (CAP)
  • postal_codes (string, nullable) - Range of postal codes for large municipalities (e.g., "00118-00199")
  • created_at, updated_at, deleted_at

Relationships

  • Region โ†’ hasMany โ†’ Province
  • Province โ†’ belongsTo โ†’ Region
  • Province โ†’ hasMany โ†’ Municipality
  • Municipality โ†’ belongsTo โ†’ Province

Replacing Existing Command

If you already have a geography:import command in your project, you can replace it with the package's command:

// In app/Console/Kernel.php or in your existing command
Artisan::command('geography:import', function () {
    $this->info('Starting geographical data import...');

    try {
        $count = \PlinCode\IstatGeography\Facades\IstatGeography::import();
        $this->info("Import completed successfully! Imported {$count} municipalities.");
    } catch (\Exception $e) {
        $this->error('Error during import: ' . $e->getMessage());
    }
})->purpose('Import regions, provinces and municipalities from ISTAT');

Testing

Run the test suite:

composer test

The package includes:

  • โœ… Unit tests for models and relationships
  • โœ… Feature tests for the import service
  • โœ… Feature tests for the update command and services
  • โœ… Mocked HTTP requests (no external dependencies)
  • โœ… PHPStan static analysis
  • โœ… Pest PHP testing framework

Test Coverage

  • Models and their relationships
  • Import service with CSV processing
  • Compare service for detecting changes
  • Update service for applying changes
  • Artisan command functionality (import and update)
  • Configuration handling

Data Sources

ISTAT Data

Geographic data (regions, provinces, municipalities) is sourced from ISTAT (Italian National Institute of Statistics), the official Italian government statistics agency.

Postal Codes (CAP)

Postal code data is sourced from Zornade Data Downloads.

A huge thanks to Zornade for their incredible work in making Italian public data freely available. Their dedication to open data helps developers build better applications for Italian users.

Contributing

  1. Fork the project
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

The MIT License (MIT). Please see License File for more information.