Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Geolocation Laravel Package

amethyst/geolocation

Geolocation utilities for Laravel applications, offering a simple way to resolve, store, and work with location data such as coordinates, addresses, and distance calculations within your PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: amethyst/geolocation has 0 stars and 0 dependents. It is effectively unproven, undocumented, and probably a thin wrapper around a third-party IP-geolocation HTTP API. Treat the assessment below as "how to evaluate and use it safely," not "adopt blindly." Before you write any application code, spend 15 minutes reading the source.

Step 1 — Install and inspect, not integrate.

composer require amethyst/geolocation
composer show amethyst/geolocation   # version, requires, abandoned flag
ls vendor/amethyst/geolocation/src

The single most valuable file in this package is its source directory. Look for, in this order:

  1. The client/facade class — this is the object you'll actually call.
  2. A config file (config/*.php) — usually where the API endpoint, key, or driver is chosen. If it exists, publish it: php artisan vendor:publish --provider="Amethyst\Geolocation\...".
  3. A service provider — if present and Laravel auto-discovery is enabled, the package will register itself; if the provider is not in extra.laravel.providers, register it manually in config/app.php (or bootstrap/providers.php on Laravel 11+).
  4. Tests — if there are none, assume every method is untested and write your own smoke test against a known IP before shipping.

First use case — the one that pays for itself: resolve a visitor's IP to a country code on their first request, cache it on the session, and use it to set default locale and currency. That's a single call, one time per user, and it's the highest-value/lowest-risk way to try this package.

use Amethyst\Geolocation\...; // confirm the exact namespace in src/

$location = app(Geolocation::class)->getLocation($request->ip());

$country = $location?->getCountryCode(); // 'US', 'GB', ...

Verify against a known IP (8.8.8.8 → US, 1.1.1.1 → AU, 81.2.69.142 → GB) before wiring it into anything. If those three work, the package does its job.

Implementation Patterns

Wrap it. Never call the vendor class directly from controllers.

The whole risk profile of this package is that it's a 0-star dependency. Put it behind your own interface so replacing it with stevebauman/location, geocoder-php, or a local MaxMind DB is a one-file change:

namespace App\Support\Geo;

interface GeoResolver
{
    public function country(string $ip): ?string;
    public function city(string $ip): ?string;
}

final class AmethystResolver implements GeoResolver
{
    public function __construct(private \Amethyst\Geolocation\... $client) {}

    public function country(string $ip): ?string
    {
        return $this->client->getLocation($ip)?->getCountryCode();
    }
    // ...
}

Bind it in a service provider ($this->app->bind(GeoResolver::class, AmethystResolver::class)). Every call site now depends on your interface, not the vendor.

Cache aggressively — this is non-negotiable.

IP-to-location lookups are an external HTTP round-trip. An IP's location changes on the order of months, not seconds. A 24-hour cache is conservative; a week is fine.

public function country(string $ip): ?string
{
    return Cache::remember("geo:country:{$ip}", now()->addDay(), function () use ($ip) {
        return $this->client->getLocation($ip)?->getCountryCode();
    });
}

Better yet, persist to a ip_lookups table (ip, country_code, region, city, resolved_at) and check that first. DB hit is ~0.1ms; an API call is 100–800ms. This also gives you a free audit trail and lets you re-resolve stale rows in a queued job rather than on a user's request.

Resolve once per session, not per request.

Use middleware that only fires when the session has no geo data yet:

public function handle($request, Closure $next)
{
    if (! session()->has('geo.country')) {
        session(['geo.country' => $this->resolver->country($request->ip())]);
    }
    return $next($request);
}

Guard this middleware with auth-optional routes only, and never put it in front of webhook or API endpoints — you'll be making an outbound HTTP call inside a webhook handler, which is exactly where you don't want latency.

Store ISO 3166-1 alpha-2 codes, not city strings.

country_code is stable, joinable to your own countries table, and translatable. City names come back spelled inconsistently across providers ("São Paulo" vs "Sao Paulo") and change whenever the vendor rotates their dataset.

Backfill in the queue, never in-line.

For historical users, dispatch a job that resolves in batches with a rate limiter:

RateLimiter::for('geo', fn () => Limit::perMinute(40));

Most free IP APIs cap you at 45/minute. Blowing through that gets you a 429 and a support ticket.

Test with a stub, not the network.

$this->instance(GeoResolver::class, new class implements GeoResolver {
    public function country(string $ip): ?string { return 'GB'; }
    public function city(string $ip): ?string { return 'London'; }
});

If the package uses Laravel's HTTP client under the hood, Http::fake([...]) works too — check src/ to confirm whether it uses GuzzleHttp\Client directly (in which case you'll need Guzzle's MockHandler).

Gotchas and Tips

Private and reserved IPs will break you. 127.0.0.1, ::1, 10.x.x.x, 192.168.x.x, and Docker bridge IPs will either return null, return the location of the API server, or throw. Filter before you call:

if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
    return null; // fall back to 'US' or your default locale
}

This is the #1 cause of "it works in production but local dev shows me in Kansas" bug reports.

Behind a load balancer, $request->ip() is wrong unless you configure it. Set TrustProxies middleware (or $middleware->trustProxies(at: '*') on Laravel 11) and confirm $request->ip() returns the real client IP, not the ALB's. If it returns the proxy IP, every user resolves to the same data center — and your cache will happily serve that wrong answer to everyone.

Accuracy is asymmetric: country is reliable, city is a coin flip. Any IP API — this one included — is generally >99% correct on country and often 50–70% on city. Don't build a feature that depends on city correctness (e.g., "show stores within your city"). Country + optional region is the safe contract.

Latency is real and unbounded. There's no guarantee this package sets a timeout on its HTTP client. If it doesn't, a slow API response becomes a slow page load. If you can reach the underlying client, set timeout => 2 and connect_timeout => 1 on the Guzzle/HTTP client. If you can't, wrap calls in rescue() (or try/catch) and treat any failure as "unknown country" rather than a 500:

$country = rescue(fn () => $this->resolver->country($ip), null, false);

GDPR/CCPA: an IP address is personal data under GDPR. Combining it with a stored city/region makes it more so. Don't log raw IPs alongside resolved location at debug level, hash IPs you persist, and give users a way to clear it. If you're storing ip_lookups, add a retention policy.

Version-pin and vendor-read. With zero dependents and likely zero users, there is no community pressure to fix bugs. Pin an exact version in composer.json ("amethyst/geolocation": "1.0.*", not ^1.0), and if the package proves essential, copy the 2–3 relevant classes into app/Support/Geo and drop the dependency. The API surface you actually need is almost certainly getLocation($ip) plus three getters.

Debugging checklist when a lookup returns nonsense:

  • Log the raw response body before parsing — the package may swallow an API error into an empty object.
  • Check whether the vendor endpoint requires an API key you haven't configured (read the config file and the README, in that order — the README is 20 lines at best).
  • Test the endpoint directly with curl using the same IP. If curl works and the package doesn't, it's a parsing/config issue, not a network one.
  • Watch for IPv6: 2001:4860:4860::8888 should resolve to the US. If it returns null, the package only handles IPv4 — a real limitation you need to know about before you enable IPv6 on your load balancer.

Best extension point: the driver/config abstraction, if it has one. If the package hardcodes a single API endpoint, don't fight it — wrap it in your GeoResolver interface, add a MaxMind/GeoIP2 fallback behind the same interface, and treat the Amethyst package as one implementation among several. That way an abandoned package is an afternoon of work, not a rewrite.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib