-
Notifications
You must be signed in to change notification settings - Fork 59
Open
Labels
Description
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
- Zero Conversion Code - Automatic bidirectional conversion between GeoJSON and PostGIS
- Type Safety - Built-in validation of GeoJSON structure
- REST API Ready - Direct serialization to JSON responses
- JavaScript Integration - Works seamlessly with Leaflet, Mapbox, OpenLayers
- Standard Compliant - Follows RFC 7946 GeoJSON specification
- Performance - Uses efficient PostGIS native functions