Skip to content
stevebaumanPublic

About

Retrieve a visitor's location from their IP address.

Topics

Resources

Stars

1.3k stars

Watchers

27 watching

Forks

Latest commit

 

History

645 Commits

Folders and files

Repository files navigation

Location

Retrieve a visitor's location from their IP address using various services (online & local).

Build status Number of downloads MIT license

Index

Requirements

  • PHP >= 8.1
  • Laravel >= 8.0

Installation

Install location using composer require:

composer require stevebauman/location

Publish the configuration file (this will create a location.php file inside the config directory):

php artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider"

Usage

Retrieve a client's location

use Stevebauman\Location\Facades\Location;

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

Important:

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

Location::get() returns a Stevebauman\Location\Position instance, or false when no driver could locate the IP address. With the default IpApi driver, the position looks like this:

$position = Location::get('8.8.8.8');

$position->toArray();
[
    'ip' => '8.8.8.8',
    'driver' => 'Stevebauman\Location\Drivers\IpApi',
    'countryName' => 'United States',
    'currencyCode' => 'USD',
    'countryCode' => 'US',
    'regionCode' => 'VA',
    'regionName' => 'Virginia',
    'cityName' => 'Ashburn',
    'zipCode' => '20149',
    'isoCode' => null,
    'postalCode' => '20149',
    'latitude' => '39.03',
    'longitude' => '-77.5',
    'metroCode' => null,
    'areaCode' => null,
    'timezone' => 'America/New_York',
]

All values are strings, including latitude and longitude. A field the driver doesn't provide is null. See Available Fields for what each driver returns.

Retrieve the location of a specific IP address

$position = Location::get('8.8.8.8');

Testing

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

use Stevebauman\Location\Position;
use Stevebauman\Location\Facades\Location;

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

// Somewhere in your application...

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

If you prefer, you may use an asterisk to return the same position for any IP address that is given:

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

$position = Location::get($anyIpAddress); // Position

If no expectations are given, or an expectation is not matched, Location::get will return false:

Location::fake();

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

If your application attempts to retrieve the location's of multiple IP addresses, you may provide multiple IP address expectation patterns:

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

You may also use an asterisk to fake several IP patterns:

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

Drivers

Available Drivers

Driver Token (.env) HTTPS Notes
IpApi - No Default driver. The free API is for non-commercial use and is rate limited.
IpApiPro IP_API_TOKEN Yes Paid version of IpApi, same fields.
IpData IPDATA_TOKEN Yes Token required.
IpInfo IPINFO_TOKEN (optional) No Doesn't return the country name.
IpInfo Lite IPINFO_TOKEN Yes Country only.
Kloudend KLOUDEND_TOKEN (optional) Yes
GeoPlugin - No The free API has been discontinued, requests without a paid plan return 403.
MaxMind MAXMIND_LICENSE_KEY - Reads a local database, no request per lookup. The web service also needs MAXMIND_USER_ID.
Cloudflare - - Reads the headers Cloudflare adds to the request, so the app must be behind Cloudflare and the IP passed to Location::get() is ignored.
IP2Location.io IP2LOCATIONIO_TOKEN Yes

Available Fields

Every position has ip and driver. The other fields depend on the driver:

Field IpApi / Pro IpData IpInfo IpInfo Lite Kloudend GeoPlugin MaxMind City MaxMind Country Cloudflare IP2Location.io
countryName ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
countryCode ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
isoCode ✓ ✓ ✓
currencyCode ✓ ✓ ✓ ✓ ¹
regionCode ✓ ✓ ✓ ✓ ✓ ✓ ² ✓ ¹
regionName ✓ ✓ ✓ ✓ ✓ ✓ ✓ ² ✓
cityName ✓ ✓ ✓ ✓ ✓ ✓ ✓ ² ✓
postalCode ✓ ✓ ✓ ✓ ✓ ✓ ² ✓
latitude, longitude ✓ ✓ ✓ ✓ ✓ ✓ ✓ ² ✓
timezone ✓ ✓ ✓ ✓ ✓ ✓ ✓ ² ✓ ³
metroCode ✓ ⁴ ✓ ¹
areaCode ✓ ✓ ¹
  1. Only on paid IP2Location.io plans, null on the free plan.
  2. Only when the "Add visitor location headers" Managed Transform is enabled in Cloudflare. Without it, only the country is available.
  3. A UTC offset such as -07:00, not a timezone name like the other drivers.
  4. Deprecated by MaxMind.

zipCode is deprecated. It holds the same value as postalCode.

Setting up MaxMind with a self-hosted database (optional)

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.

To set up MaxMind to retrieve the user's location from your own server, you must:

  1. Create a maxmind account and sign in
  2. Click "Manage License Keys" from the profile menu dropdown
  3. Generate a new license key
  4. Copy the license key and save it into your .env file using the MAXMIND_LICENSE_KEY key
  5. Run php artisan location:update to download the latest .mmdb file into your database/maxmind directory
  6. That's it, you're all set!

Note: Keep in mind, you'll need to update this file by running location:update on a regular basis to retrieve the most current information from your visitors.

Sharing a MaxMind database across servers

When an application runs on multiple servers, the MaxMind database may be stored on a shared Laravel filesystem disk. Each server caches the shared database on its local filesystem because the MaxMind reader requires a local file path.

// config/location.php

'maxmind' => [
    // ...

    'storage' => [
        'disk' => env('MAXMIND_STORAGE_DISK'),
        'path' => env('MAXMIND_STORAGE_PATH', 'maxmind/GeoLite2-City.mmdb'),
        'ttl' => env('MAXMIND_STORAGE_TTL', 3600),
    ],

    'local' => [
        'type' => 'city',
        'path' => storage_path('app/maxmind/GeoLite2-City.mmdb'),
        // ...
    ],
],

For example, the database may be stored on the application's s3 disk:

MAXMIND_STORAGE_DISK=s3
MAXMIND_STORAGE_PATH=maxmind/GeoLite2-City.mmdb
MAXMIND_STORAGE_TTL=3600

The ttl value is the number of seconds a local database may be used before the configured disk is checked for a newer version.

Only one server should be scheduled to download and publish the database from MaxMind. Laravel's onOneServer method may be used when every server uses the same lock-capable cache store:

use Illuminate\Support\Facades\Schedule;

Schedule::command('location:update')
    ->daily()
    ->onOneServer();

Other servers will download the database from the shared disk when it is first needed and refresh their local copy after its TTL has elapsed.

Each server coordinates refreshes with a lock beside its local database. While one worker refreshes the database, other workers continue using the existing copy. If a refresh fails, the existing database is retained and another attempt is made after the configured TTL. Downloads are validated before replacing the local copy.

Localized MaxMind names

MaxMind returns countryName, regionName and cityName in English by default. To get them in another language, set the locales list. It applies to both the local database and the web service:

// config/location.php

'maxmind' => [
    'web' => [
        // ...
        'locales' => ['de'],
    ],
],

Supported codes: de, en, es, fr, ja, pt-BR, ru, zh-CN (use pt-BR, not pt_BR). English is appended when missing, so an untranslated name falls back to English instead of null. If you cache results, include the locale in the cache key.

Only the MaxMind driver uses this setting. Other drivers return names in the language of their API.

Fallback Drivers

In the config file, you can specify as many fallback drivers as you wish. It is recommended to configure the MaxMind driver with the local database .mmdb file (mentioned above), so you are alwaysretrieving some generic location information from the visitor.

If an exception occurs trying to grab a driver (such as a 400/500 error if the providers API changes), it will automatically use the next driver in line.

Timeouts

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:

'total_timeout' => 5,

Creating your own drivers

To create your own driver, simply create a class in your application, and extend the abstract Driver:

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;
    }
}

Then, insert your driver class name into the configuration file:

// config/location.php

'driver' => App\Location\Drivers\MyDriver::class,

Versioning

Location is versioned under the Semantic Versioning guidelines as much as possible.

Releases will be numbered with the following format:

<major>.<minor>.<patch>

And constructed with the following guidelines:

  • Breaking backward compatibility bumps the major and resets the minor and patch.
  • New additions without breaking backward compatibility bumps the minor and resets the patch.
  • Bug fixes and misc changes bumps the patch.

Minor versions are not maintained individually, and you're encouraged to upgrade through to the next minor version.

Major versions are maintained individually through separate branches.

About

Retrieve a visitor's location from their IP address.

Topics

Resources

Stars

1.3k stars

Watchers

27 watching

Forks

Releases

Packages

Used by

Contributors

Languages