toin0u/geocoder-laravel
Geocoder for Laravel integrates the Geocoder PHP library into Laravel apps, enabling address geocoding and reverse geocoding with configurable providers and caching. Requires PHP 8.2+ and Laravel 11+, with service provider auto-discovery for quick setup.
This package is a thin Laravel service-provider wrapper around geocoder-php/geocoder. Its job is to give you one container-resolved object—app('geocoder') / Geocoder::—that can geocode addresses, reverse-geocode coordinates, and resolve IPs, with Laravel-native HTTP and caching underneath.
Minimum path to first successful call:
composer require toin0u/geocoder-laravel (requires PHP ≥ 8.2, Laravel ≥ 11).php artisan vendor:publish --provider="Geocoder\Laravel\Providers\GeocoderService" --tag="config".GOOGLE_MAPS_API_KEY (and GOOGLE_MAPS_LOCALE, which is a Google TLD-style region value, not a language code) in .env.$addresses = app('geocoder')->geocode('Los Angeles, CA')->get(); // Illuminate\Support\Collection
$coords = $addresses->first()?->getCoordinates();
Read these first, in order: the Upgrading note at the top of the README (13.0.0 is breaking, and the default HTTP adapter changed to LaravelHttpClient), then your published config/geocoder.php (providers/chain order, cache-duration, adapter, reader), then the upstream Geocoder docs for the specific provider you're wiring in. The config file is the whole API surface; almost every surprise lives there.
First real use case: a checkout form or store-locator where you take a free-text address, store lat/lng, and later reverse-geocode a user's IP for a default region. Do that end-to-end before adding more providers.
Wrap it in your own service, don't sprinkle app('geocoder') everywhere. The container binding is convenient but untyped in practice; inject Geocoder\Laravel\ProviderAndDumperAggregator into a small GeocodingService and expose domain methods (coordinatesFor(string $address): ?Coordinates, addressFor(float $lat, float $lng): ?Address). That gives you one place to add logging, metrics, and retries, and one place to swap providers later.
Exploit the Chain provider for resilience. Default config chains GoogleMaps → GeoPlugin. First provider to return wins; a failing provider falls through. You can also pin a provider for a single call:
app('geocoder')->using('google_maps')->geocode($address)->get();
Use the chain for addresses, but pin geo_plugin for IP lookups so you never burn a paid Google call on an IP string.
Treat caching as a first-class feature, not an optimization. Geocoding is a metered, slow, external dependency. Follow the README's dedicated-store recommendation (separate Redis DB → cache.stores.geocode → config/geocoder.php's cache.store) so a cache:clear on your app store doesn't nuke your geocode corpus, and vice versa. On queries where freshness matters, opt out per-call:
app('geocoder')->doNotCache()->geocode($address)->get();
Also add the recommended post-update-cmd/post-install-cmd cache:clear hooks—stale serialized Address collections are a classic post-deploy bug.
Geocode at the edge of your model lifecycle, not in the request path. A model observer on saving that fills lat/lng when the address fields changed is the cleanest pattern; back it with a geocoded_at timestamp so you don't re-hit the API on every save. For bulk imports or user-triggered lookups, push to a queued job—geocoding 500 rows synchronously will blow past your request timeout, especially with retries configured.
Localize deliberately. locale('it')->geocode('Bologna')->get() caches separately per locale, so if you support multiple languages, each locale is a cold cache on day one. If your app switches locale per user, warm the important queries or accept the multiplier on API calls.
Use GeocodeQuery/ReverseQuery when you need options beyond locale—limits, country filters, address components:
use Geocoder\Query\GeocodeQuery;
$query = GeocodeQuery::create('Springfield')
->withLocale('en')
->withLimit(1);
app('geocoder')->geocodeQuery($query)->get();
Test with Http::fake()—this is the biggest win of the 13.x adapter change. Because LaravelHttpClient delegates to Laravel's Http facade, you can fake provider endpoints and assert what was sent:
Http::fake(['maps.googleapis.com/*' => Http::response($fixture, 200)]);
$result = app('geocoder')->doNotCache()->geocode('Los Angeles, CA')->get();
Http::assertSent(fn ($request) => str_contains($request->url(), 'maps.googleapis.com'));
Run the test suite against the array cache driver (or doNotCache()) so cached results from one test don't leak into the next.
Configure the adapter for production traffic. The default has no timeout—set one, plus retries, in config:
'adapter' => [LaravelHttpClient::class => [
'timeout' => 10,
'connectTimeout' => 3,
'retry' => [3, 100],
]],
Keep verify on; the README's ['verify' => false] example should be treated as a local-debug escape hatch, never a deployed default.
composer update across 5.x → 13.x without reading the upgrading section; the default HTTP adapter changed.cache-duration may be silently zero. If you published the config in an older version, the key is missing and caching defaults to 0—everything looks fine but you're hitting the provider every time. Check the key exists and is 9999999 (or your intended value), then php artisan config:clear.cache.serializable_classes. The package auto-scans vendor/geocoder-php/* and merges model classes into the allow-list at boot. That is a deliberate, narrow relaxation of a security feature. If your team audits deserialization surface, set auto_register_serializable_classes => false and either maintain the allow-list yourself or disable geocoder caching entirely—otherwise you risk __PHP_Incomplete_Class objects coming back out of cache.region/locale semantics: Google uses TLDs for region and a fixed language list—wrong values get silently ignored or degrade results.using(), locale(), and doNotCache() mutate the resolved instance. If geocoder is a shared singleton, a doNotCache() in one code path can affect later calls in the same request or in a long-lived queue worker. Keep these calls local to the service method that needs them, or re-resolve.get() returns an empty Collection, not null. Every "address not found" bug starts with calling ->first()->getCoordinates(). Always ->isEmpty() / null-safe chain, and be aware some providers return approximate or multiple matches—withLimit(1) keeps the payload small but you're taking whatever the provider ranks first.doNotCache()'s inverse (check your cache store first) for critical paths.Psr\Http\Client\ClientInterface class in adapter (Curl client with proxy options, or your own retrying decorator); reader in config for file-backed providers like GeoIp2; and adding/removing provider classes in providers—the chain shape is just nested config arrays keyed by class name, so you can nest multiple chains or place providers alongside the chain.env() in the published config is baked at config:cache time. Rotating an API key requires a config recache, not just an .env edit—worth a note in your deploy runbook.How can I help you explore Laravel packages today?