feat: #2924446: Port Spatial Tools to Drupal 11 and Drupal 12

Closes #2924446

Summary

This replaces the Drupal 7 code with a Drupal 11.2+ / 12 version. Format support no longer comes from one submodule per format but from GDAL drivers, so zipped shapefiles, KML, GeoJSON, GeoPackage and any other vector format GDAL reads are supported.

The only backend for now runs the GDAL command line tools (ogr2ogr, ogrinfo). Backends are plugins, so a future backend that calls libgdal through PHP FFI (in this module or in a separate contrib module) can be added as a new plugin class without changing the public API, the value objects or consumer code.

Target branch: this MR is meant for a new 2.0.x branch, which does not exist yet. Could a maintainer create 2.0.x from 7.x-1.x and retarget the MR?

Requirements

  • Drupal ^11.2 || ^12, PHP 8.3+.
  • GDAL 3.6+ command line tools on the web server (apt-get install gdal-bin).
  • The spatial_geofield submodule also needs Geofield.

Architecture

  • Value objects (src/Model, final and immutable): Feature (id, decoded GeoJSON geometry, properties), LayerInfo, ReadOptions (layer, target SRS, limit, source SRS override).
  • SpatialReader plugin type (PHP attributes, discovered in Plugin/SpatialReader of any module) with SpatialReaderInterface: isAvailable(), getVersion(), getLayers(), readFeatures(). readFeatures() is a generator that streams features. Backends only receive validated absolute local paths.
  • spatial.reader picks the configured backend or, with auto, the available one with the highest weight. The CLI backend has weight 0.
  • spatial.file_reader is the only entry point for consumers. It accepts a file entity or a stream wrapper URI, resolves it to a local path, and rejects anything else (including /vsi* paths and remote wrappers).
  • SpatialReaderException carries backend-neutral error codes (backend unavailable, invalid input, cannot open, layer not found, no layers, missing source SRS, invalid SRS, timeout, invalid output), so consumers can react without parsing backend messages.
  • SpatialReaderDiagnosticsInterface (optional) lets a backend explain why it is unavailable.
  • GdalPathTrait maps paths to GDAL dataset names (/vsizip/, /vsigzip/) for any GDAL-based backend.

CLI backend (ogr2ogr_cli)

  • Commands are always built as argument arrays for Symfony Process, never as shell strings.
  • Output is read incrementally from the process iterator, one JSON feature per line, and never buffered as a whole. The process is stopped if the consumer stops iterating.
  • Layers come from ogrinfo -json -so on GDAL 3.7+ and from the text output on GDAL 3.6.
  • GDAL's GeoJSONSeq driver always reprojects to EPSG:4326 and ignores -t_srs. It is used when the target is EPSG:4326. For any other target, or to keep the source SRS, the backend streams the GeoJSON driver's output (RFC7946=NO), which also writes one feature per line.
  • With no layer given, the first layer is read explicitly: without a layer name, ogr2ogr would write all layers.

Admin UI

  • Settings at /admin/config/system/spatial: backend, ogr2ogr and ogrinfo paths, timeout. The form lists the status of every backend, and after saving it checks the new settings right away.
  • The status report shows the backend and GDAL version, or why no backend is available (for example "GDAL 3.4.1 was found, but 3.6 or newer is required").

spatial_geofield submodule

A "Spatial file upload" widget for Geofield fields:

  • Two modes: one field value per feature (respecting cardinality) or one GEOMETRYCOLLECTION value.
  • Reads all layers by default (or only the first one).
  • Optional source SRS for files without one, such as shapefiles without .prj.
  • Rejects coordinates outside the longitude/latitude range, which usually means the file's SRS is missing or wrong.
  • Reads the file as soon as it is uploaded and previews what it found (geometry count per type, layers) or what is wrong, before saving.
  • Read errors become clear messages without server paths. Site administrators also get pointers to the settings.

Tests

  • SpatialReaderContractTestBase: an abstract kernel test that any backend, including a future FFI one, extends. It covers layers, feature counts, properties, geometry types, reprojection, source SRS override, limit, and error codes against GeoJSON, zipped shapefile, KML, a shapefile without .prj and a corrupt file. The fixtures and the exact commands used to generate them are in tests/fixtures.
  • Ogr2ogrCliReaderTest runs the contract and is skipped when GDAL is missing.
  • Unit tests for path validation, resolver, GdalPathTrait and the ogrinfo parsers (including GDAL 3.6 output).
  • BackendDiagnosticsTest uses fake executables, so it needs no GDAL.
  • Kernel and functional tests for the widget: value building, error messages, preview, upload and save without JavaScript.
  • GitLab CI uses the drupal.org templates (phpcs, phpstan level 6, phpunit on current Drupal 11 and the next major) and installs gdal-bin so the CLI tests run.

Locally: 63 tests pass on Drupal 11.4.10, PHP 8.4, PostgreSQL 18 and GDAL 3.13.2. phpcs (Drupal, DrupalPractice) and phpstan report no errors.

Not verified yet

  • Drupal 12: Geofield does not declare ^12 yet, so CI uses the lenient Composer plugin for it.
  • GDAL 3.6 against a real installation (only its output format is unit tested).
  • MySQL and SQLite.
  • The AJAX (JavaScript) upload path of the widget.

Merge request reports

Loading
Loading