# location resolves an IP address through a chain of Laravel geolocation drivers

> Location is an MIT licensed Laravel package that turns an IP address into a country, region and city using one of eleven geolocation providers, with a fallback chain, per-driver timeouts and a shared total timeout. It can also use a self-hosted MaxMind database, and it ships a fake() helper so tests never touch the network.

**stevebauman/location** — Retrieve a visitor's location from their IP address.

- Repository: https://github.com/stevebauman/location
- Stars: 1,311 · Forks: 204
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/stevebauman-location

## One facade, eleven providers, and a fallback chain

Location retrieves a visitor's location from their IP address, and the public interface is a facade with two methods.

```php
use Stevebauman\Location\Facades\Location;

if ($position = Location::get()) {
    // Successfully retrieved position.
    echo $position->countryName;
} else {
    // Failed retrieving position.
}
```

The second method takes an explicit address.

```php
$position = Location::get('192.168.1.1');
```

So the package covers two distinct uses, resolving the current request's visitor and resolving an arbitrary address you already have, and the second is the one that turns it into a component rather than a demo.

The drivers list is the substance of the package. IpApi is the default, with IpApiPro, IpData, IpInfo, IpInfo Lite, Kloudend, GeoPlugin, MaxMind, Cloudflare, IP2Location.io and Kloudend's sibling at ipapi.co, plus IP2Location.io. Eleven providers, and the point of having eleven is the fallback chain.

The config file lets you specify as many fallback drivers as you wish, and the README recommends configuring the MaxMind driver with the local database .mmdb file, so you are always retrieving some generic location information from the visitor. If an exception occurs trying to grab a driver, such as a 400/500 error if the provider's API changes, it will automatically use the next driver in line.

That last sentence describes a real failure mode, because these are free tier APIs with rate limits and undocumented breaking changes, and a single-provider implementation turns a provider outage into a broken page. The chain is the resilience mechanism, and the recommended local database is the floor that the chain falls back to when every network provider has been tried.

The Position object is what comes back, and it is a set of named properties like countryName, countryCode, region, city and the rest, which is a more useful shape than an array and is what makes the fake() helper in the testing section possible.

## The default configuration fakes the location, and that is deliberate

There is an important note in the usage section, and it is the kind of thing that catches people out.

This method retrieves the user's IP address via request()->ip(). In the default configuration, testing.enabled is active, and the returned IP address is in the USA. Disable it to get the client's real IP address.

By default, a lookup without an explicit IP substitutes the configured test IP. The package still resolves that IP using its configured drivers, which may call an external service; this setting is not a guarantee that network requests are disabled. Explicit IP arguments take precedence over the test IP. Disable testing for real visitor lookups and review the selected providers before deployment.

There is a second, more technical reason to be careful here, and it is not in the README but follows from what request()->ip() does in a Laravel application behind a proxy or a load balancer. Laravel will return the address it believes the client used. Behind a reverse proxy, configure only trusted proxies and ensure they overwrite untrusted forwarding headers; otherwise the address can be wrong or spoofed. A location package that resolves the wrong address gives a wrong country with no error, and the fix is in the application configuration rather than in the package.

The MaxMind setup is the escape hatch for teams that do not want to call out to a third party at all, and the README is explicit that it is encouraged to set up MaxMind as a fallback driver using a local database so that some location information is returned in the event of hitting a rate limit from the external web services.

The steps are the usual MaxMind ones. Create a maxmind account and sign in, click Manage License Keys from the profile menu dropdown, generate a new license key, copy it into your .env file using the MAXMIND_LICENSE_KEY key, run php artisan location:update to download the latest .mmdb file into your database/maxmind directory, and you are set.

The note that follows is a real operational obligation rather than a formality. You will need to update this file by running location:update on a regular basis to retrieve the most current information from your visitors. A stale GeoLite database is a silently wrong database, so the download step needs to be in a scheduled task, the same way the certificate renewal would be.

## Timeouts are per driver, and a bad provider costs you five times over

The timeout configuration is the section that most directly affects how the package behaves in production, and the README is unusually blunt about the cost.

The http config option times out a single driver's request. Each driver gets its own, so with the default configuration an unreachable provider costs you that timeout five times over before the lookup gives up.

Set total_timeout to give the whole lookup one shared budget instead.

```php
'total_timeout' => 5,
```

That is a small feature with a large effect. The per-driver timeout is the obvious design, each provider gets a fair slice, but the consequence is that the worst case for a lookup is the sum of the timeouts of every driver in the chain, and with a default configuration of five drivers that is five times the timeout. A page that renders a location lookup on every request would, in the failure case, hold a request open for that multiplied duration.

The total_timeout option changes the shape of that. Now the whole lookup has one budget. A slow first driver can consume the budget, leaving no time for later network providers; the caller can bound the overall lookup time.

It is worth being clear that this is a trade rather than a fix. A per-driver timeout gives each provider its own chance, and a total timeout can starve a later provider if an earlier one is slow. For a lookup that is a nice-to-have on a page, the total budget is the right choice, and for a lookup that must succeed, the per-driver timeout with a local database fallback is the right choice.

The design overall is a good example of a small package thinking about its failure modes. The failure modes here are not exceptions in the normal sense, they are a provider being rate limited, a provider changing its API, and a network path being down, and the package addresses all three, with a chain, a local fallback and two levels of timeout.

## Writing a driver is one class and one config line

The extension point is a documented class, and the example in the README shows the whole contract.

```php
namespace App\Location\Drivers;

use Illuminate\Support\Fluent;
use Illuminate\Support\Facades\Http;
use Stevebauman\Location\Position;
use Stevebauman\Location\Request;
use Stevebauman\Location\Drivers\Driver;

class MyDriver extends Driver
{    
    protected function process(Request $request): Fluent
    {
         $response = Http::get("https://driver-url.com", ['ip' => $request->getIp()]);
         
         return new Fluent($response->json());
    }

    protected function hydrate(Position $position, Fluent $location): Position
    {
        $position->countryCode = $location->country_code;

        return $position;
    }
}
```

And then the config.

```php
// config/location.php
'driver' => App\Location\Drivers\MyDriver::class,
```

Two methods, and the split between them is the design worth understanding. The process method does the work of talking to the provider and returns a Fluent of the raw response, so it deals in the provider's own vocabulary. The hydrate method maps that Fluent onto the Position object, so it deals in the package's vocabulary. A provider whose field names differ, country_code versus countryCode, is therefore a one-line change in hydrate rather than a change in the logic of the lookup.

The use of Illuminate\Support\Fluent on both sides is what makes that possible. A Fluent is an object with arbitrary keys, so the raw response passes through without being shaped, and the Position is a fixed object with named properties, so the consumer always gets the same shape regardless of which provider answered.

And the request object is a package type, Stevebauman\Location\Request, which means the driver gets the IP through a method rather than reaching for the Laravel request singleton, so a driver is testable and a driver can be used for an arbitrary address rather than only the current visitor.

The package uses Laravel's own Http facade inside the driver, so an application with a configured HTTP client, its own timeouts, its own retry policy and its own logging gets those for free in the lookup as well as anywhere else in the app.

## The test fake is the reason this package is usable in a test suite

The testing section is the most thoughtful part of the README, and the reason is that a package like this would otherwise break every test that renders a page.

You may call Location::fake with an array of IP address patterns and their expected positions to fake the location of an IP address.

```php
use Stevebauman\Location\Position;
use Stevebauman\Location\Facades\Location;

Location::fake([
    '127.0.0.1' => Position::make([
        'countryName' => 'United States',
        'countryCode' => 'US',
        // ...
    ])
]);

$position = Location::get('127.0.0.1'); // Position
```

The patterns are matched with wildcards, so a range of addresses can share an expectation.

```php
Location::fake([
    '192.123.*.*' => Position::make([
        'countryName' => 'United States',
        'countryCode' => 'US',
    ]),
    '192.456.*.*' => Position::make([
        'countryName' => 'Canada',
        'countryCode' => 'CA',
    ]),
]);
```

And a single asterisk matches everything, which is the case you want when the test does not care about the address at all.

The failure behaviour is also specified rather than left to chance. If no expectations are given, or an expectation is not matched, Location::get will return false.

```php
Location::fake();

Location::get($anyIpAddress); // false
```

So an unmatched address returns false rather than making a real network call, and the same false the production code checks for is what the test sees. That consistency is what makes the fake useful, because a test double that returns a different shape from the real thing is worse than no double at all.

The Position::make constructor takes an array and leaves the rest of the properties unset, which is why the examples show a comment where the other fields would go, and a test that only asserts on countryName does not have to construct a full position.

## Version 7 was a rewrite of the driver contract

The upgrading section is short and it names the changes.

In version 7, the codebase has been updated with strict PHP types, updated PHP and Laravel version requirements, an updated Driver structure, as well as a small config change.

The requirements are stated at the top of the README, PHP >= 8.1 and Laravel >= 8.0, and the module path is stevebauman/location, so the package is a Laravel service rather than a standalone library with a framework adapter.

The installation is the standard Laravel two-step. The composer require, and then publishing the config file with php artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider", which creates a location.php file inside the config directory.

The driver structure change is the one that matters for anyone extending the package, and the Driver example in the README is the new shape. A process method that takes a Request and returns a Fluent, and a hydrate method that takes a Position and a Fluent and returns a Position. Strict types across the codebase means the return types are enforced rather than documented, so a driver that returns the wrong shape fails at the call site instead of producing a position with missing fields.

The repository layout matches the package structure. There is a src/ directory for the implementation, a config/ directory with the location.php configuration, a tests/ directory with a phpunit.xml, a composer.json, and a readme.md.

The release history shows a project that is actively maintained but released on its own schedule rather than with the framework. v7.6.2 came out in February 2026, v7.6.3 in March, and v7.7.0 on 2026-10-05 with the last push on the same day, so there was a long gap in the middle of the year, which is the shape of a package maintained alongside other work by one author.

## Where location is the wrong choice

The first wrong case is anything needing accurate city-level data. The default driver is IpApi, a free tier API, and the free tiers of these services are coarse, often country and region only, and the accuracy degrades further for the addresses that are hardest to place, mobile carrier ranges, corporate NAT ranges and anything behind a VPN. If your application shows a store locator or a local price, this package will frustrate you, and the answer is a paid provider or a self-hosted database with a commercial licence.

The second wrong case is an application where the privacy cost is not worth the feature. Calling a third party API with the visitor's IP address discloses the visitor's address and your traffic to that provider. A privacy conscious application, or one in a jurisdiction with specific rules about disclosing visitor data, might not want that at all, and the MaxMind local database option reduces the disclosure to a lookup against your own server.

The third wrong case is a team that does not want the request to depend on an external service being up. Even with a fallback chain, a page that renders a location lookup synchronously has a path where the response is slow, and the default configuration can hold a request open for five driver timeouts. A team that has decided location is a nice-to-have should either give it a total timeout or compute it asynchronously.

The fourth wrong case is a non-Laravel project, or a Laravel project on an old version. The package requires Laravel 8.0 or newer and PHP 8.1, and it is a service provider with a config file and a facade, so it is not a drop-in for a Symfony or a Slim application.

## How it differs from calling a geolocation API directly

The comparison worth making is with the alternative of putting an HTTP call to ip-api.com in your controller, and the difference in approach is where the resilience lives.

A direct call is three lines and it is completely clear what it does. It also fails in three ways. The provider is rate limited and returns an error, and the page has no country. The provider changes its response format, and the code silently reads a missing key. And the provider's schema differs from every other provider, so switching to a better one is a rewrite of the parsing.

Location moves those three problems into the package. The fallback chain handles the rate limit and the outage, and the driver structure handles the schema, because hydrate is the only place a provider's field names appear. And the Position object is the same shape whichever provider answered, so the consumer code does not change when you switch.

The second difference is the local database. The MaxMind driver with a .mmdb file on your own server is not just a fallback, it is a different kind of service, no outbound request, a predictable latency and a cost that does not scale with traffic. Putting it in the chain means the network providers are the optimisation and the local database is the floor, which is the opposite of the usual arrangement and the more robust one.

The third difference is the total timeout, and the direct call has no equivalent because there is no chain. The package lets a caller bound the entire lookup rather than each attempt.

The fourth difference is the test fake. A direct call in a controller has to be mocked at the HTTP layer, which means your tests depend on the provider's response format, so a provider change breaks your test suite. Location::fake works in terms of the package's own Position type, so the tests stay stable while the providers change underneath, and the failing case, an unmatched address, returns the same false that production returns.

The cost is the dependency and the abstraction. You now have a package between your code and the provider, a config file, a service provider, and eleven possible providers to reason about, for a task that a direct call does in three lines. That is a good trade for a team with more than one application and a shared need, and a poor one for a single feature in a single codebase.

## Conclusion

location fits a Laravel application that wants a visitor's country or region for analytics, content localisation or compliance, and it fits a team that wants a MaxMind local database as a fallback so a rate-limited provider does not leave the page without a country. It does not fit an application that needs accurate city-level data, since the free providers the package defaults to are coarse at best, and it does not fit a project that has no need for the visitor's location at all. Before adopting it, note that the default configuration has testing.enabled active so the returned IP is in the USA, disable it to get the real client address, and that the current release is v7.7.0 from 2026-10-05 with the last push on the same day.

## FAQ

### How do I install and configure the location package?

Run composer require stevebauman/location, then publish the config with php artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider", which creates a location.php file in the config directory. The requirements are PHP 8.1 or newer and Laravel 8.0 or newer.

### Which geolocation providers does location support?

Eleven, with IpApi as the default: IpApi, IpApiPro, IpData, IpInfo, IpInfo Lite, Kloudend, GeoPlugin, MaxMind, Cloudflare, IP2Location.io and the ipapi.co service. You can configure as many fallback drivers as you want in the config file, and the package uses the next one in line if an exception occurs.

### Why does location return a United States position by default?

Because in the default configuration testing.enabled is active, so the returned IP address is in the USA and no request leaves your server. The README says to disable it to get the client's real IP address. The method retrieves the user's address via request()->ip(), so behind a proxy you also need Laravel to trust the forwarded headers.

### How do I use a self-hosted MaxMind database with location?

Create a MaxMind account, generate a license key from the Manage License Keys menu, put it in your .env file as MAXMIND_LICENSE_KEY, then run php artisan location:update to download the .mmdb file into your database/maxmind directory. The README notes you need to run location:update regularly to keep the data current.

### How do I test code that uses location?

Call Location::fake with an array mapping IP address patterns to Position::make arrays, and the patterns accept wildcards such as 192.123.*.* or a single asterisk for any address. If no expectations are given or an expectation is not matched, Location::get returns false, the same value production code checks for.

## Sources

- [Issues](https://github.com/stevebauman/location/issues)
- [License: MIT](https://github.com/stevebauman/location/blob/master/LICENSE)
- [README](https://github.com/stevebauman/location/blob/master/README.md)
- [Releases](https://github.com/stevebauman/location/releases)
- [stevebauman/location on GitHub](https://github.com/stevebauman/location)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/stevebauman-location
