utafitilabs / postgis-bundle
PostGIS spatial types, automatic GiST indexing, and ST_* DQL functions for Doctrine ORM 3 / DBAL 4 on Symfony.
Package info
github.com/utafitilabs/postgis-bundle
Type:symfony-bundle
pkg:composer/utafitilabs/postgis-bundle
Requires
- php: >=8.4
- doctrine/dbal: ^4.0
- doctrine/doctrine-bundle: ^3.0
- doctrine/orm: ^3.5
- symfony/framework-bundle: ^7.3 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.2
- symfony/dependency-injection: ^7.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 13:27:09 UTC
README
This bundle gives Doctrine ORM 3 / DBAL 4 first-class PostGIS support: spatial column
types exchanged as GeoJSON, ST_* DQL functions, automatic GiST indexing, and
typed geometry columns with churn-free migrations — no hand-written spatial SQL,
no configuration. Enable the bundle and go.
Part of the utafiti tools.
Contents
Why
Doctrine ships no spatial types, and PostGIS stores every shape as the base geometry
type — so the usual approaches either hand-write DDL or fight migrations:diff churn.
This bundle handles all of it: declare a column, get a GiST-indexed, GeoJSON-friendly,
optionally shape-constrained column with clean diffs.
Install
Applications using Symfony Flex:
composer require utafitilabs/postgis-bundle
Applications without Symfony Flex — after requiring the package, enable the bundle:
// config/bundles.php return [ // ... UtafitiLabs\PostGISBundle\UtafitiLabsPostGISBundle::class => ['all' => true], ];
That one line registers the spatial types, the DB-type mappings, the ST_* DQL functions,
the USING gist platform middleware, the typmod-aware schema manager, the auto-GiST
schema listener, and a schema_filter that keeps PostGIS's own tables and sequences
(spatial_ref_sys, the topology schema) out of migrations:diff. Nothing else to configure.
The bundle's configuration root key is utafiti_labs_post_gis. It takes no options — it
is simply the key reserved in config/packages/, and the prefix of the bundle's internal
service ids.
An application that sets its own doctrine.dbal.schema_filter replaces the bundle's; keep
PostGIS's assets out in that expression too
(how).
A taste
use Doctrine\ORM\Mapping as ORM; class Area { // Generic geometry — accepts any shape, SRID 4326, diff-clean. #[ORM\Column(type: 'geometry')] public ?string $footprint = null; // GeoJSON string in, GeoJSON string out // Typed — PostGIS enforces the shape via the typmod, still diff-clean. #[ORM\Column(type: 'multipolygon')] public ?string $boundary = null; // geometry(MultiPolygon,4326) }
$em->createQuery( 'SELECT COUNT(a.id) FROM App\Entity\Area a WHERE ST_Intersects(a.boundary, ST_GeomFromGeoJSON(:poly)) = true' )->setParameter('poly', $geoJsonPolygon)->getSingleScalarResult();
Column types: geometry, geography, point, linestring, polygon, multipolygon.
DQL functions: ST_AsGeoJSON, ST_GeomFromGeoJSON, ST_Intersects, ST_DWithin,
ST_Distance, ST_Length, ST_Area, ST_Buffer, ST_Intersection, ST_Union (aggregate
and pairwise), ST_SimplifyPreserveTopology, ST_MakeValid, ST_CollectionExtract,
ST_Multi, ST_KnnDistance(a, b) (the <-> operator, for index-ordered nearest-neighbour
lists) and Geography(g) (the ::geography cast, for geodesic measurement:
ST_Area(Geography(t.geom)) = m², ST_Distance(Geography(t.geom), ST_GeomFromGeoJSON(:p))
= metres) — enough to express dissolve-style aggregation and distance ranking entirely in DQL.
Prefer no DQL at all? Extend the repository base and the St methods just exist:
use UtafitiLabs\PostGISBundle\Repository\SpatialEntityRepository; final class AreaRepository extends SpatialEntityRepository {} // that's all $areas->findStIntersecting($geoJson); // GiST-backed ST_Intersects $areas->findStWithinMetres($geoJson, 500); // within 500 m, nearest first, GiST-backed $areas->stAreaKm2(['source' => 'wdpa']); // geodesic km², findBy-style criteria
Every method carries the St marker — the bundle's signature, never confusable
with a Doctrine core method. The geometry column is discovered from the entity's
own metadata (subclasses read it with getGeometryField()); an entity without one
is rejected at construction with a teaching message.
Every geometry/geography column gets a USING gist index in generated migrations, automatically.
Documentation
Read the documentation at docs/index.md — including
generic vs. typed geometry columns and how typed columns
stay migrations:diff-clean.
Requirements
- PHP 8.4+ · Symfony 7.3+ / 8
doctrine/dbal^4,doctrine/orm^3.5,doctrine/doctrine-bundle^3- PostgreSQL with the PostGIS extension
Contributing
See CONTRIBUTING.md. CI enforces the standard (php-cs-fixer, PHPStan max, PHPUnit against real PostGIS, lowest→newest dependency matrix) on every pull request.
Credits
License
MIT License (MIT): see the LICENSE file for more details.