Although there are many developers that say that the geo-location should be done through the browser, there are many situations where you can't use the Geolocation API of the browser. This, independently from your reasons, can be solved by getting the most important information about an user's IP like its country, city and language. However, from where and how can you retrieve such information from only the IP? There are many third party services, some of them paid that allow you to get such information using a REST API. You send the IP of the user as argument and you will retrieve information about it. However, there are an important factor that prevents a lot of developers to use such APIs, the most important of it is having in mind that everytime an user visits the website, a request will be executed to the REST API to get the geo information. For a sane mind, this is usually an innecesary approach.
That's why there are other options, like the one based on databases. One of the most known free alternatives of this approach is the GeoLite2 database by MaxMind. In this article, we'll show you how to get the country, city and locale from a visitor's IP using the GeoLite2 database in your Symfony project.
Advantages of this approach over usage of rest APIs
The sanest reason why you would have a geo database locally in your server is that you won't need to execute request to any third party server. Instead, you are quering a local database, where the performance relies only on your server and not on the connection nor availability of third party services. You have a database near to you to query information about the user's ip, the situation couldn't be clearer.
1. Download GeoLite2 Free Databases
As first step you will need the binary database of GeoLite in your project or accesible at system level. You can download the database from the official MaxMind GeoLite2 free here:
In this tutorial we'll use and include the database in our own project in the /private
directory of our symfony project (note that this directory doesn't exist and therefore needs to be created, you can change the path of the database according to your needs) and we'll use the City version of the database that allows us to get the information that we mentioned in this article, specifically the City, Country and Locale of the visitor's IP. The databases are compressed using tar, so you would be able to extract its content from the command line using the following command:
tar -xzf GeoLite2-City_20180206.tar.gz
Alternatively in other operative systems like Windows you could simply extract its content using 7Zip, Winrar or another decompression tool. Now that you have the database in your project, the directory structure should look like:
Note
If there are multiple projects that use the database, you may want to place the database in other place where all the projects can have access to it. As in this tutorial we only need the database in this project, we are placing it inside of the project as well.
The GeoLite2-City.mmdb filesize is around 63MB and it contains all the information that you need to deduce the information that you want from the user's IP.
2. Install MaxMind GeoIP2 PHP API
In order to read the database, you won't need to host in MySQL or other database manager. The Database has a special format from the creators of GeoIP namely MaxMind DB. The MaxMind DB file format is a database format that maps IPv4 and IPv6 addresses to data records using an efficient binary search tree. The binary database is split into three parts:
- The binary search tree. Each level of the tree corresponds to a single bit in the 128 bit representation of an IPv6 address.
- The data section. These are the values returned to the client for a specific IP address, e.g. “US”, “New York”, or a more complex map type made up of multiple fields.
- Database metadata. Information about the database itself.
For more information about the type of database used by this project, please read more about MaxMind DB here.
Now, we need a parser for this database format. Fortunately, the MaxMind team wrote an awesome library for PHP that makes the interaction with the database pretty easy and you will be able to retrieve geo information about an user's IP with only a couple of lines of code. We are talking about the MaxMind GeoIP PHP Api, a package that provides an API for the GeoIP2 web services and databases. The API also works with the free GeoLite2 databases (the one that we're using). You can install this package in your Symfony project with composer running the following command:
composer require geoip2/geoip2
For more information about this library, please visit the official repository at Github here. After the installation of the package you will be able to use its classes on your controllers of Symfony.
3. Retrieving Geo Information about an user's IP from a controller
Now that you have the database and the wrapper to read information from it, you can simply proceed to request information from the database about a specified IP. The logic to achieve this is the following, declare a variable that contains the absolute (local) path to the GeoLite2-City database. This needs to be providen as first argument in a new instance of the Reader class of GeoIp2 that you will use to query the information about the IP. Finally execute the city method from the created instance providing the IP from which you want information as first argument. You need obviously to provide a valid IP or the reader will throw an exception, obviously you need to be prepared as well that the user's IP may not be recognized by the database, so you need to catch the AddressNotFoundException
when you query information of an IP:
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
// Include GeoIp2 Classes
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
// Declare the path to the GeoLite2-City.mmdb file (database)
$GeoLiteDatabasePath = $this->get('kernel')->getRootDir() . '/../private/geolite2-city/GeoLite2-City.mmdb';
// Create an instance of the Reader of GeoIp2 and provide as first argument
// the path to the database file
$reader = new Reader($GeoLiteDatabasePath);
try{
// if you are in the production environment you can retrieve the
// user's IP with $request->getClientIp()
// Note that in a development environment 127.0.0.1 will
// throw the AddressNotFoundException
// In this example, use a fixed IP address in Minnesota
$record = $reader->city('128.101.101.101');
} catch (AddressNotFoundException $ex) {
// Couldn't retrieve geo information from the given IP
return new Response("It wasn't possible to retrieve information about the providen IP");
}
return new JsonResponse($record);
}
}
In our example with the given IP, you will get a JSON response the following output in the browser:
{
"city": {
"geoname_id": 5037649,
"names": {
"de": "Minneapolis",
"en": "Minneapolis",
"es": "Mineápolis",
"fr": "Minneapolis",
"ja": "ããã¢ããªã¹",
"pt-BR": "Minneapolis",
"ru": "ÐиннеаполиÑ",
"zh-CN": "æå°¼é¿æ³¢å©æ¯"
}
},
"continent": {
"code": "NA",
"geoname_id": 6255149,
"names": {
"de": "Nordamerika",
"en": "North America",
"es": "Norteamérica",
"fr": "Amérique du Nord",
"ja": "åã¢ã¡ãªã«",
"pt-BR": "América do Norte",
"ru": "СевеÑÐ½Ð°Ñ ÐмеÑика",
"zh-CN": "åç¾æ´²"
}
},
"country": {
"geoname_id": 6252001,
"iso_code": "US",
"names": {
"de": "USA",
"en": "United States",
"es": "Estados Unidos",
"fr": "États-Unis",
"ja": "ã¢ã¡ãªã«åè¡å½",
"pt-BR": "Estados Unidos",
"ru": "СШÐ",
"zh-CN": "ç¾å½"
}
},
"location": {
"accuracy_radius": 5,
"latitude": 44.9759,
"longitude": -93.2166,
"metro_code": 613,
"time_zone": "America/Chicago"
},
"postal": {
"code": "55414"
},
"registered_country": {
"geoname_id": 6252001,
"iso_code": "US",
"names": {
"de": "USA",
"en": "United States",
"es": "Estados Unidos",
"fr": "États-Unis",
"ja": "ã¢ã¡ãªã«åè¡å½",
"pt-BR": "Estados Unidos",
"ru": "СШÐ",
"zh-CN": "ç¾å½"
}
},
"subdivisions": [
{
"geoname_id": 5037779,
"iso_code": "MN",
"names": {
"en": "Minnesota",
"es": "Minnesota",
"fr": "Minnesota",
"ja": "ããã½ã¿å·",
"pt-BR": "Minesota",
"ru": "ÐиннеÑоÑа",
"zh-CN": "æå°¼èè¾¾å·"
}
}
],
"traits": {
"ip_address": "128.101.101.101"
}
}
Note that the $record
variable is a stdClass object, so there's no getter methods. You can retrieve the properties from the object in PHP as follows:
print($record->country->isoCode . "\n"); // 'US'
print($record->country->name . "\n"); // 'United States'
print($record->country->names['zh-CN'] . "\n"); // 'ç¾å½'
print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
print($record->city->name . "\n"); // 'Minneapolis'
print($record->postal->code . "\n"); // '55455'
print($record->location->latitude . "\n"); // 44.9733
print($record->location->longitude . "\n"); // -93.2323
MaxMind does not provide official support for the free GeoLite2 databases. If you have questions about the GeoLite2 databases or GeoIP2 APIs, please see stackoverflow’s GeoIP questions and answers.
Happy coding !