Skip to content

Add Native GeoJSON Type Support #82

@maxhelias

Description

@maxhelias

GeoJSON is the de facto standard for representing geographic features in web applications. Currently, this library requires users to manually convert between GeoJSON and PostGIS formats (WKT/WKB), which is cumbersome and error-prone.

This feature request proposes adding a native geojson Doctrine type that:

  • Automatically converts between PHP arrays (GeoJSON) and PostGIS geometry
  • Uses ST_GeomFromGeoJSON() and ST_AsGeoJSON() for efficient conversion
  • Provides seamless integration with REST APIs and JavaScript mapping libraries

Example of implementation (source : Claude) :

<?php
namespace Jsor\Doctrine\PostGIS\Types;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * GeoJSON Type for Doctrine
 * 
 * Stores geometry data as PostGIS GEOMETRY but converts to/from GeoJSON format.
 * Provides seamless integration with web APIs and JavaScript mapping libraries.
 * 
 * @see https://tools.ietf.org/html/rfc7946 GeoJSON Specification (RFC 7946)
 * @see https://postgis.net/docs/ST_GeomFromGeoJSON.html PostGIS ST_GeomFromGeoJSON
 * @see https://postgis.net/docs/ST_AsGeoJSON.html PostGIS ST_AsGeoJSON
 */
class GeoJSONType extends Type
{
    const GEOJSON = 'geojson';
    
    /**
     * @inheritDoc
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        // Default to generic GEOMETRY, can be overridden with options
        $geometryType = $column['geometry_type'] ?? 'Geometry';
        $srid = $column['srid'] ?? 4326;
        
        return sprintf('GEOMETRY(%s, %d)', $geometryType, $srid);
    }
    
    /**
     * Convert PHP value (array) to database value (GeoJSON string)
     * 
     * @param array|string|null $value
     * @param AbstractPlatform $platform
     * @return string|null
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
    {
        if ($value === null) {
            return null;
        }
        
        // If already a string (WKT/GeoJSON), return as-is
        if (is_string($value)) {
            return $value;
        }
        
        // If array, convert to JSON string
        if (is_array($value)) {
            $this->validateGeoJSON($value);
            return json_encode($value, JSON_THROW_ON_ERROR);
        }
        
        throw new \InvalidArgumentException(
            'GeoJSON value must be an array or JSON string'
        );
    }
    
    /**
     * Wrap database value in ST_GeomFromGeoJSON() function
     * 
     * @param string $sqlExpr
     * @param AbstractPlatform $platform
     * @return string
     */
    public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform): string
    {
        return sprintf('ST_GeomFromGeoJSON(%s)', $sqlExpr);
    }
    
    /**
     * Convert database value (GeoJSON string) to PHP value (array)
     * 
     * @param string|resource|null $value
     * @param AbstractPlatform $platform
     * @return array|null
     */
    public function convertToPHPValue($value, AbstractPlatform $platform): ?array
    {
        if ($value === null) {
            return null;
        }
        
        // Handle binary/stream values
        if (is_resource($value)) {
            $value = stream_get_contents($value);
        }
        
        // Parse JSON to array
        $geoJson = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
        
        // Validate structure
        $this->validateGeoJSON($geoJson);
        
        return $geoJson;
    }
    
    /**
     * Wrap SQL expression with ST_AsGeoJSON() for reading
     * 
     * @param string $sqlExpr
     * @param AbstractPlatform $platform
     * @return string
     */
    public function convertToPHPValueSQL($sqlExpr, $platform): string
    {
        // Use ST_AsGeoJSON with default options (no bbox, precision 15)
        return sprintf('ST_AsGeoJSON(%s)', $sqlExpr);
    }
    
    /**
     * @inheritDoc
     */
    public function getName(): string
    {
        return self::GEOJSON;
    }
    
    /**
     * @inheritDoc
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
    
    /**
     * Validate GeoJSON structure
     * 
     * @param array $geoJson
     * @throws \InvalidArgumentException
     */
    private function validateGeoJSON(array $geoJson): void
    {
        if (!isset($geoJson['type'])) {
            throw new \InvalidArgumentException(
                'GeoJSON must have a "type" property'
            );
        }
        
        $validTypes = [
            'Point', 'MultiPoint', 
            'LineString', 'MultiLineString',
            'Polygon', 'MultiPolygon',
            'GeometryCollection', 'Feature', 'FeatureCollection'
        ];
        
        if (!in_array($geoJson['type'], $validTypes, true)) {
            throw new \InvalidArgumentException(
                sprintf('Invalid GeoJSON type: %s', $geoJson['type'])
            );
        }
        
        // For Geometry types, coordinates are required
        if ($geoJson['type'] !== 'GeometryCollection' 
            && $geoJson['type'] !== 'Feature'
            && $geoJson['type'] !== 'FeatureCollection'
            && !isset($geoJson['coordinates'])
        ) {
            throw new \InvalidArgumentException(
                sprintf('GeoJSON type "%s" must have "coordinates"', $geoJson['type'])
            );
        }
    }
}

And with enhanced options (source : claude) :

/**
 * Extended version with additional ST_AsGeoJSON options
 */
class GeoJSONType extends Type
{
    // ... previous code ...
    
    /**
     * Options for ST_AsGeoJSON output
     */
    private array $options = [
        'maxdecimaldigits' => 9,  // Coordinate precision
        'options' => 0,            // Bitmask options (bbox, short CRS, etc.)
    ];
    
    public function convertToPHPValueSQL($sqlExpr, $platform): string
    {
        // ST_AsGeoJSON(geom, maxdecimaldigits, options)
        return sprintf(
            'ST_AsGeoJSON(%s, %d, %d)',
            $sqlExpr,
            $this->options['maxdecimaldigits'],
            $this->options['options']
        );
    }
    
    /**
     * Set precision for coordinate output
     */
    public function setCoordinatePrecision(int $digits): self
    {
        $this->options['maxdecimaldigits'] = $digits;
        return $this;
    }
    
    /**
     * Include bounding box in GeoJSON output
     */
    public function includeBBox(bool $include = true): self
    {
        if ($include) {
            $this->options['options'] |= 1;  // Set bit 0
        } else {
            $this->options['options'] &= ~1; // Unset bit 0
        }
        return $this;
    }
}

Benefits

  1. Zero Conversion Code - Automatic bidirectional conversion between GeoJSON and PostGIS
  2. Type Safety - Built-in validation of GeoJSON structure
  3. REST API Ready - Direct serialization to JSON responses
  4. JavaScript Integration - Works seamlessly with Leaflet, Mapbox, OpenLayers
  5. Standard Compliant - Follows RFC 7946 GeoJSON specification
  6. Performance - Uses efficient PostGIS native functions

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions