phil/geolocation-bundle
A Symfony bundle for adding geolocation support to PHP applications. It provides configurable services for resolving and working with location data, making it easier to integrate geolocation features without building the underlying plumbing yourself.
Read this first: do not composer require this into a Laravel app. ph-il/geolocation-bundle is a Symfony2 bundle (PHP 5.4/5.5 era, Doctrine 2.x), last released January 2015, archived, and has zero dependents. It won't autoload into Laravel, brings no service provider, and pulls Symfony2 FrameworkBundle/DependencyInjection expectations you'd have to shim. Treat it as a reference implementation to read, not a dependency to install.
What it actually gives you (worth mining):
SELECT ... ORDER BY DISTANCE(...) inside DQL instead of raw SQL.Where to look in the repo, in order:
README.md — install snippet, bundle registration, mapping config. Short but shows the intended workflow.Doctrine/ or DQL/) — this is the only part with real value. The generated SQL is the Haversine formula written as a SQL snippet; copy it.Entity/ and any Resources/config/doctrine/*.orm.xml — shows the field types (decimal vs float) and indexes they chose.Trait/Interface files — the "geolocatable entity" contract.First use case to port: "find the 20 nearest stores to this postcode." That's a bounding-box prefilter plus a Haversine ORDER BY, and it's what 90% of these bundles exist for. In Laravel that's a query scope plus ST_Distance_Sphere (MySQL 5.7+) or PostGIS ST_DWithin — roughly 30 lines. Read the bundle's DQL function to confirm the formula, then write it natively.
Entity trait + interface. The bundle's core idea: an interface (GeolocatableInterface-style) plus a trait supplying latitude/longitude getters/setters. Port it as a PHP trait and an Eloquent interface:
trait HasCoordinates {
public function scopeWithinBox($q, float $lat, float $lng, float $km) { /* ... */ }
public function scopeOrderByDistance($q, float $lat, float $lng) { /* ... */ }
}
Use decimal(10,7) for latitude and decimal(10,7) for longitude — not float. Floats bite you on equality and on index-only scans.
Address as a separate aggregate. The bundle separates Address from the geolocatable entity on purpose: one address can be geocoded once and shared, and the geocode result (lat/lng + provider + accuracy) is metadata about the address, not about the user. Keep that separation. Geocode on write, cache the result keyed by a normalized address hash, never geocode in a request loop.
Custom DQL functions. Registration pattern in Symfony2 was:
doctrine:
orm:
dql:
numeric_functions:
DISTANCE: Phil\GeolocationBundle\Doctrine\...\Distance
The Laravel equivalent is Doctrine\ORM\Configuration::addCustomNumericFunction() if you're using Doctrine via laravel-doctrine, or just an Eloquent scope. Don't register the bundle's function as-is — the emitted SQL is dialect-specific (written against MySQL of that era) and will not run on Postgres or on MySQL 8 with ONLY_FULL_GROUP_BY.
Bounding-box prefilter before Haversine. The pattern that matters: compute a lat/lng box (lat ± km/111, lng ± km/(111*cos(lat))), index (latitude, longitude), filter with a cheap BETWEEN, then sort by the expensive trig expression on the small result set. Pure Haversine over a full table is a sequential scan; this bundle's docs gloss over that and it's the #1 reason people think geo queries are "slow."
Integration tips for a Laravel migration:
sqrt(...).geocoded_at and geocoder column. You will re-geocode, and you'll want to know which records used a bad provider.round($km, 1)) in the presenter, never in SQL.Archived means archived. No security patches, no PHP 8 compatibility, no Doctrine 3 support. Even in a legacy Symfony 2.8 app, budget for patching the DQL function yourself. Do not add it to a new project under any circumstances.
Symfony2 vs Laravel is a hard wall. There's no bridge, no GeoServiceProvider, and the bundle assumes AppKernel::registerBundles() plus YAML/XML Doctrine mapping. You'd spend longer shimming than rewriting. The realistic play is a half-day port of the two useful pieces (trait + distance SQL).
The DQL function is dialect-locked. Check which SQL it emits before trusting it. The classic bug: it uses RADIANS()/ACOS() with a LEAST(1, ...) guard — and without that guard, floating-point drift makes ACOS of 1.0000001 return NULL, silently dropping rows from your results. Always clamp: ACOS(LEAST(1, GREATEST(-1, ...))).
Degrees vs radians. The most common porting error. RADIANS() on the stored columns and on the bound parameters — and be explicit about which the API accepts.
Index reality. A composite B-tree on (latitude, longitude) only helps the bounding-box filter if latitude is the leading column and your box is narrow. For real spatial querying, add a POINT column with a SPATIAL INDEX (MySQL) and use ST_Distance_Sphere. The bundle predates usable MySQL spatial support and doesn't do this.
Geocoding side effects. The bundle's address flow assumes you bring your own geocoder (Google/MapQuest-era). Rate limits and ToS are on you. Cache aggressively; geocode failures should leave latitude/longitude null, not 0,0 — Null Island coordinates are the single most common bug in geo data and will silently appear in "nearest" results.
Extension points (for reading, not extending): the entity mappings are overridable via bundle config (Symfony's resolve_target_entities-style pattern), and the geocoder is injectable. Both are worth understanding as design patterns, but with 12 stars and no maintainers there's no ecosystem to extend into.
Verification checklist after porting: test the formula against a known pair (e.g. two coordinates with a published distance), confirm results are symmetric, confirm a point at distance d is included at d - ε and excluded at d + ε, and check the query plan (EXPLAIN) to prove the bounding box is actually being used.
How can I help you explore Laravel packages today?