vich/geographical-bundle
A Symfony bundle for working with geographical data: countries, states, zones, and multilingual names. It provides form types, a Doctrine-backed geographical hierarchy, and locale-aware selectors, making it easy to add country and region fields to your entities and forms.
Read this first: vich/geographical-bundle is a Symfony2 (2.0–2.8 era) bundle, not a Laravel package. There is no service provider, no facade, no publishable config. For a Laravel team the realistic outcome is one of two things: (a) you are auditing a legacy Symfony2 app that already uses it, or (b) you are mining it for a proven design pattern to reimplement with Eloquent + Blade. Do not attempt composer require vich/geographical-bundle in a Laravel 9/10/11 app — the constraints (symfony/framework-bundle ^2.x, Doctrine 2.2-era, PHP 5.3+) will not resolve.
If you are in the Symfony2 app it was built for:
composer require vich/geographical-bundle:dev-master
// app/AppKernel.php
new Vich\GeographicalBundle\VichGeographicalBundle(),
# app/config/config.yml
vich_geographical:
db_driver: orm # or mongodb
Annotate the entity — the bundle's core trick is a Doctrine event listener that geocodes a string property and writes lat/lng back to the object:
use Vich\GeographicalBundle\Mapping\Annotation as Vich;
/**
* @ORM\Entity
* @Vich\Geographical
*/
class Location
{
/** @ORM\Column(length=255) @Vich\Geographical(on="update") */
protected $address;
/** @ORM\Column(type="decimal", precision=10, scale=7) */
protected $latitude;
/** @ORM\Column(type="decimal", precision=10, scale=7) */
protected $longitude;
}
First use case / first place to look: rendering one map with a handful of markers.
// Controller
$map = $this->get('vich_geographical.map_provider')->getMap();
$map->setZoom(6);
foreach ($locations as $loc) {
$map->addMarker(new MapMarker($loc, ['title' => $loc->getName()]));
$map->addWindow(new MapWindow($loc, ['content' => $loc->getAddress()]));
}
return ['map' => $map];
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY"></script>
{{ vich_geographical_render_map(map) }}
Everything else is discovered by reading, in order: README.md → DependencyInjection/Configuration.php (all config keys) → Resources/config/*.xml (service ids) → Map/ (the object model: Map, MapMarker, MapWindow, Coordinate) → Twig/ (the rendering functions).
Laravel translation of the first use case — this is the part worth copying:
trait HasCoordinates
{
public static function bootHasCoordinates(): void
{
static::saved(function ($model) {
if ($model->isDirty('address')) {
GeocodeLocation::dispatch($model); // queued, not inline
}
});
}
}
<x-map :markers="$locations->map->only('lat','lng','name','address')" :zoom="6" />
1. Geocode-on-write, never on-read. The bundle hangs geocoding off Doctrine's postPersist/postUpdate. Copy the lifecycle hook, not the synchronous call: dispatch a job, store the result, and record a geocoded_at / geocode_status column so failures are visible instead of silently leaving null coordinates.
2. Change detection is the whole game. Only re-geocode when the address field actually changed, and beware echo loops — writing latitude/longitude and flushing inside the listener can re-trigger update. In Symfony this was handled by the listener skipping managed-entity no-ops; in Laravel use isDirty() plus saveQuietly().
3. Map as an object graph, built in a service, not in the view. Map → MapMarker → MapWindow is a clean separation: markers carry a geo entity and options (title, icon, draggable), windows carry HTML content. Keep a MapBuilder-style class per screen so the controller stays thin and the map is testable without rendering.
4. Multiple maps per page. Instantiate new Map('locations') with an explicit name/dom-id instead of the shared provider; the JS renderer keys off that id. Render all map JS in one block after the Google Maps script tag, in the same order the maps were created.
5. Extension points actually worth using:
MapRendererInterface implementation if you need OpenLayers/Leaflet output instead of Google Maps.MapMarker / MapWindow to add icon URLs, colors, clusters.app/Resources/VichGeographicalBundle/views/... (Symfony2 convention) rather than editing vendor files.6. Laravel-side equivalents of each piece:
| Bundle concept | Laravel equivalent |
|---|---|
Doctrine listener + @Vich\Geographical |
Model trait + saved hook / observer |
Map / MapMarker / MapWindow |
Plain DTOs or JsonResource payloads |
vich_geographical_render_map() |
Blade component or @push('scripts') |
map_provider service |
Container binding / MapFactory |
| Bundle template override | php artisan vendor:publish (or component slots) |
7. Testing. Bind a fake geocoder (a stub returning fixed coordinates) in the test container and assert coordinates are persisted from an address. Never let CI hit Google — it is rate-limited, keyed, and non-deterministic. In Laravel: Http::fake().
<script src="http://maps.google.com/maps/api/js?sensor=false"> snippet and keyless geocoding endpoint no longer work; you now need an API key on both the JS API and the Geocoding API, and geocoding results have caching/attribution restrictions under Google's ToS. If you port this, use Leaflet + OSM/Nominatim or a paid geocoder.null coordinates fail loudly in a bad way. A marker with no lat/lng drags the map to (0,0) off the coast of Africa. Always filter markers to rows where both coordinates are present and non-zero before building the map.decimal(10,7) (or POINT + a spatial index) — floats lose precision and string casts break - signs in some drivers. If you need "within X km", don't scan haversine in PHP; use ST_Distance_Sphere or a bounding-box prefilter.geocoded_at so you can query for un-geocoded rows in bulk rather than discovering them in the UI.vich_geographical_render_map() emits inline JS that assumes the Google Maps API is already loaded. Load the API in <head> (or make the renderer wait for DOMContentLoaded), and never render two maps with the same id.on="update" vs insert-only semantics.db_driver must match your actual mapping driver; the bundle registers separate ORM and ODM listeners. If markers never get coordinates, verify the driver value before debugging the geocoder.How can I help you explore Laravel packages today?