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_geofieldsubmodule 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). SpatialReaderplugin type (PHP attributes, discovered inPlugin/SpatialReaderof any module) withSpatialReaderInterface:isAvailable(),getVersion(),getLayers(),readFeatures().readFeatures()is a generator that streams features. Backends only receive validated absolute local paths.spatial.readerpicks the configured backend or, withauto, the available one with the highest weight. The CLI backend has weight 0.spatial.file_readeris 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).SpatialReaderExceptioncarries 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.GdalPathTraitmaps 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 -soon GDAL 3.7+ and from the text output on GDAL 3.6. - GDAL's
GeoJSONSeqdriver 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 theGeoJSONdriver'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,
ogr2ogrwould write all layers.
Admin UI
- Settings at
/admin/config/system/spatial: backend,ogr2ograndogrinfopaths, 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.prjand a corrupt file. The fixtures and the exact commands used to generate them are intests/fixtures.Ogr2ogrCliReaderTestruns the contract and is skipped when GDAL is missing.- Unit tests for path validation, resolver,
GdalPathTraitand the ogrinfo parsers (including GDAL 3.6 output). BackendDiagnosticsTestuses 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-binso 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
^12yet, 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.