happyr/location-bundle
A Symfony bundle for adding location support to your application: geocode addresses, store latitude/longitude and address details on entities, and resolve coordinates via pluggable geocoder services. Useful for building location-aware features without wiring geocoding logic yourself.
This is a Symfony bundle, not a Laravel package — composer require happyr/location-bundle in a Laravel app will fail to resolve (its symfony/* and PHP constraints predate PHP 8). So "getting started" means two things: setting up a scratch Symfony project to read the bundle in context, and identifying the three or four files that contain the actual ideas.
Where to look first, in order:
Location value object (Model/Location.php or similar). This is the core concept: latitude, longitude, and optional address components (street, postal code, city, country). Everything else is plumbing around this.Resources/config/doctrine/*.xml or PHP mapping). This shows the intended persistence model — coordinates as a value object embedded on your entity, not four loose columns. The shape of this is portable to an Eloquent cast even though the XML isn't.LocationManager / geocoder service (Services/LocationManager.php). This is the integration point: it takes an address or an IP, calls a Geocoder\Geocoder adapter, and returns a populated Location. This is the piece that tells you how provider abstraction is meant to work.Resources/config/services.yml). Read this to understand what's wired together and what the config tree exposes (provider selection, API keys).The one thing to do first: spin up a throwaway Symfony 2.8 or 3.4 project in a scratch directory, require the bundle, and get $locationManager->getLocationForAddress('...') returning a populated Location. That takes an afternoon and tells you more about the provider contract than the README does. If you'd rather not, just clone the repo and read those four files — you'll get 90% of the value in 30 minutes.
Your first real use case in Laravel: don't try to run this code. Take the value object concept — latitude, longitude, formatted address — and implement it as a small Location class in your app with an Eloquent cast backing latitude/longitude decimal columns. Then wrap geocoder-php/geocoder (the same Geocoder library family the bundle depends on, but actively maintained) in a GeocoderService. That is your day-one deliverable, and it takes a day, not a sprint.
Every pattern below translates the bundle's approach into Laravel terms. None of them involve running the bundle's code.
Value object + cast (replaces the Doctrine embeddable).
final class Location
{
public function __construct(
public readonly float $latitude,
public readonly float $longitude,
public readonly ?string $formattedAddress = null,
) {
if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) {
throw new InvalidArgumentException('Coordinate out of range');
}
}
}
Back it with a migration adding latitude DECIMAL(10,7), longitude DECIMAL(10,7), and address columns. Add an Eloquent cast if you want the object on the model, or keep it as a standalone DTO if you prefer to leave the model thin — the bundle's intent (coordinates kept together, separate from display address) is what matters.
Geocoder behind an interface (replaces the DI-wired manager).
interface GeocoderService
{
public function geocode(string $address): ?Location;
public function reverseGeocode(float $lat, float $lng): ?Location;
}
Bind an implementation in a service provider. The bundle's config-driven provider swap becomes a Laravel config value (config('services.geocoder.provider') plus match or a container binding). Keep the null-return contract — geocoding will fail silently at provider level, and the bundle assumes that too.
Decorator stack for caching and resilience. The bundle does not solve caching; you must. Wrap the real geocoder in a caching decorator keyed on normalized address:
class CachingGeocoder implements GeocoderService { /* cache->remember(...) */ }
class NullSafeGeocoder implements GeocoderService { /* try/catch -> null */ }
This is where the actual engineering effort lives — the bundle's whole contribution is the idea of a geocoder interface, which is a two-line contract.
Queue the batch work. Any non-interactive geocoding (importing a customer list, backfilling coordinates on existing records) belongs in a queued job with rate limiting and retry/backoff. geocoder-php's providers enforce per-second quota; a RateLimited queue middleware plus a tries cap is the standard Laravel answer. Never geocode inside a request cycle you don't have to.
Controller usage. Controllers should receive a GeocoderService, take an address from a validated FormRequest, and let the service decide whether it's a new location or a cache hit. The bundle explicitly keeps geocoding out of controllers — honor that.
Provider swapping as config. The bundle's single real selling point is that switching from Google to Nominatim to Mapbox is a config change. Reproduce it: bind the interface to an implementation chosen by config value, so the call sites never name a provider.
You cannot install this. Symfony 2-era constraints (symfony/* 2.x/3.x, PHP 5.5–7.x) will not resolve on PHP 8.x. composer why-not in a scratch project will confirm it; don't burn hours trying. Any "port" means rewriting all the service wiring and the Doctrine mapping, which is 90% of the bundle.
Doctrine ≠ Eloquent, and the mapping doesn't translate. The bundle's embeddable is XML/PHP Doctrine metadata; there is no equivalent Eloquent feature. You rebuild it as columns plus a cast. There's nothing to salvage except the field list.
The Geocoder dependency is the real dependency. willdurand/geocoder is what actually does the work, and it has evolved significantly since this bundle pinned it. Use geocoder-php/geocoder and its current provider packages directly; the bundle only saves you writing new GoogleMapsProvider(...) once.
Provider API keys and Terms of Service are the hidden risk. Google Maps geocoding results have strict storage and display terms — caching addresses in your database may violate them depending on the provider and plan. The bundle says nothing about this; check your provider's ToS before persisting formatted addresses. Nominatim has its own usage policy (rate limits, user-agent requirements) that a naive port will violate.
IP geolocation is city-level at best. The bundle's getLocationForIp() (when present) is a convenience that returns approximate coordinates. Do not build address-precision features on it. If your product implies precision, drop the IP path entirely.
Reverse geocoding has no canonical answer. Two providers will return different formatted addresses for the same coordinates. Pick one, store its output, and don't re-geocode on every read — both for cost and for consistency.
Cache aggressively and key it correctly. Normalize (trim, lowercase, collapse whitespace) before using an address as a cache key, or you'll pay for "10 Downing St" and "10 downing st" separately. Set a long TTL (addresses rarely change); set a shorter one for reverse lookups if you care about provider data freshness.
Spatial queries need a real index, not a PHP library. The bundle stores coordinates; it does not make WHERE distance < 5 fast. Use MySQL spatial columns (POINT with SPATIAL INDEX) or PostGIS if radius/polygon search is core. A bounding-box prefilter on indexed latitude/longitude decimal columns is a cheap fallback but degrades past ~hundreds of thousands of rows.
Watch out for SRID and precision mismatches. Storing DECIMAL(10,7) and querying against a computed POINT can produce errors that are hard to spot in testing and obvious in production as off-by-hundreds-of-metres distances. Pick one representation and stick to it end to end.
Failure mode to design for: provider outage. Wrap geocoding calls so a provider 500 or rate-limit degrades to "location unknown" rather than a stalled page. A circuit breaker (or even a simple try/catch returning null plus a logged warning) is required — the bundle assumes the caller handles it, and callers routinely don't.
The bundle is documentation, not a dependency. Read it for the Location-value-object-plus-service pattern; then close the tab and build the ~200-line equivalent in Laravel against geocoder-php. You'll inherit none of the EOL constraints, get modern provider adapters, and be able to move radius search into PostGIS without a fork.
How can I help you explore Laravel packages today?