PHP IP To Location

Converting an IP address into some useful location information can be useful if you want to find out where sites are hosted or customise content to users depending on their location.

All this code is freely available over at github.

There are several ways to do this, all of which have their advantages and disadvantages, but sticking with one can cause rewriting a lot of code in the future. So rather than pick one and stick with it I decided to use dependency injection to allow different classes to be used that convert IP addresses to locations in different ways. The first task is to create an abstract class that will be used to construct the rest of the IP location classes. Each class that extends this abstract class will contain a method called getIpLocation() that will convert an IP address into a location, and a method that will update the data source for the location lookup. Rather than lump all of the classes into a single directory I have created a directory called Service, into which all of the different classes that lookup IP addresses will be kept.

Using a typical Zend Framework naming convention the Abstract class in the Service directory would be called IpLocation_Service_Abstract. Here is the IpLocation_Service_Abstract class in full.


abstract class IpLocation_Service_Abstract
{
    /**
     * Convert an IP address into an integer value for data lookup.
     *
     * @param string $ip The IP address to be converted.
     *
     * @return integer The converted IP address.
     */
    protected function convertIpToDecimal($ip)
    {
        $ip = explode(".", $ip);
        return ($ip[0]*16777216) + ($ip[1]*65536) + ($ip[2]*256) + ($ip[3]);
    }
 
    /**
     * Lookup an IP address and return a IpLocation_Results object containing 
     * the data found.
     *
     * @param string $ip The IP address to lookup.
     *
     * @return boolean|IpLocation_Results The location in the form of an
     *                                    IpLocation_Results object. False
     *                                    if result is not found.
     */
    abstract public function getIpLocation($ip);
 
    /**
     * Update IP location data.
     *
     * @return boolean True if update sucessful. Otherwise false.     
     */
    abstract public function updateData();
}

In order to allow a simple interface for our IP lookup service classes I have created a class called IpLocation_Ip. This class will do some IP validation before calling a function called getIpLocation() inside the service class and returning the result.

class IpLocation_Ip
{
    /**
     * @var string The last IP address converted.
     */
    public $ip;
 
    /**
     * @var IpLocation_Results The location object.
     */
    public $results;
 
    /**
     * @var IpLocation_Service_Abstract The location service to use when
     *                                  converting IP addresses into locations.
     */
    private $_ipLocationService;
 
    /**
     * IpLocation_Ip
     *
     * @param IpLocation_Service_Abstract $locationService The location service
     *                                                     to use in this lookup.
     */
    public function IpLocation_Ip(IpLocation_Service_Abstract $locationService)
    {
        $this->_ipLocationService = $locationService;
    }
 
    /**
     * Use the location service to lookup the IP address location and return the
     * result object.
     *
     * @param string $ip The ip address to lookup.
     *
     * @return string The location
     */
    public function getIpLocation($ip)
    {
        if ($this->validateIp($ip) === false) {
            return false;
        }
 
        $this->results = $this->_ipLocationService->getIpLocation($ip);
        return $this->results;
    }
 
    /**
     * Validate IP address
     *
     * @param string $ip The IP address
     *
     * @return boolean True if IP address is valid.
     */
    public function validateIp($ip)
    {
        if (!preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/D', $ip)) {
            return false;
        }
 
        $octets = explode('.', $ip);
 
        for ($i = 1; $i < 5; $i++) {
            $octet = (int)$octets[($i-1)];
            if ($i === 1) {
                if ($octet > 223 OR $octet < 1) {
                    return false;
                }
            } elseif ($i === 4) {
                if ($octet < 1) {
                    return false;
                }
            } else {
                if ($octet > 254) {
                    return false;
                }
            }
        }
 
        return true;
    }
}

The final step is to start creating the classes that will extend the abstract class. These classes will all take an IP address and return some data, but rather than returning an array, the classes will return a standard object called IpLocation_Results. Creating this class will help in the future if we ever want to extend the amount of data that a class returns but keep the existing functionality intact. Here is the IpLocation_Results object.

class IpLocation_Results
{
    /**
     * @var array The data of the IP conversion.
     */
    private $_results = array(
        'ip'           => '',
        'country2Char' => '',
        'countryName'  => '',
    );
 
    /**
     * Constructor.
     *
     * @param string $ip           The IP address.
     * @param string $country2Char 2 character code for the country.
     * @param string $countryName  The name of the country.
     */
    public function IpLocation_Results($ip, $country2Char, $countryName)
    {
        $this->_results['ip']           = $ip;
        $this->_results['country2Char'] = $country2Char;
        $this->_results['countryName']  = $countryName;
    }
 
    /**
     * Get value.
     *
     * @param string $name The name of the value to return
     *
     * @return null|string The value if the name is present.
     */
    public function __get($name)
    {
        if (!isset($this->_results[$name])) {
            return null;
        }
        return $this->_results[$name];
    }
}

A common way to convert between IP address and location is to use the PEAR extension Geo_IP. So a good place to start is to create a service class that uses Geo_IP to do our IP to location conversion.

class IpLocation_Service_GeoIp extends IpLocation_Service_Abstract
{
    /**
     * The location of the data update file.
     *
     * @var string
     */
    protected $updateFile = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz';
 
    /**
     * The location of the data update file.
     *
     * @var string
     */
    protected $geoIpDatFile = 'GeoIP.dat';
    
    /**
     * IpLocation_Service_GeoIp
     */
    public function IpLocation_Service_GeoIp()
    {
    }
 
    /**
     * Lookup an IP address and return a IpLocation_Results object containing 
     * the data found.
     *
     * @param string $ip The ip address to lookup.
     *
     * @return string The location
     */
    public function getIpLocation($ip)
    {
        // Create Net_GeoIP object.
        $geoip = Net_GeoIP::getInstance(
            dirname(__FILE__) . '/data/' . $this->geoIpDatFile
        );
 
        try {
            $country2Char = $geoip->lookupCountryCode($ip);
            $countryName  = strtoupper($geoip->lookupCountryName($ip));
        } catch (Exception $e) {
            return false;
        }
 
        if ($country2Char == '' || $countryName == '') {
            return false;
        }
 
        return new IpLocation_Results($ip, $country2Char, strtoupper($countryName));
    }
 
    /**
     * Update the datafile.
     *
     * @return boolean True if file update sucessful.
     */
    public function updateData() 
    {
        $update = file_get_contents($this->updateFile);
 
        if (strlen($update) < 2) {
            return false;
        }
 
        if (!$handle = fopen('tmp.dat.gz', 'wb')) {
            return false;
        }
 
        if (fwrite($handle, $update) == false) {
            return false;
        }
 
        fclose($handle);
 
        $FileOpen = fopen('tmp.dat.gz', "rb");
        fseek($FileOpen, -4, SEEK_END);
        $buf = fread($FileOpen, 4);
        $GZFileSize = end(unpack("V", $buf));
        fclose($FileOpen);
 
        $gzhandle = gzopen('tmp.dat.gz', "rb");
        $contents = gzread($gzhandle, $GZFileSize);
 
        gzclose($gzhandle);
 
        $fp  = fopen(dirname(__FILE__) . "/data/" . $this->geoIpDatFile, 'wb');
        fwrite($fp, $contents);
        fclose($fp);
 
        // Delete the tmp file.
        unlink('tmp.dat.gz');
 
        return true;
    }
}

These classes are used by creating an instance of Ip_Location_Ip and injecting a new instance of a IP lookup service into the contructor. The getIpLocation() method, if given a well formed IP address, will lookup this IP address and return either a IpLocation_Results object containing the location, or false if anything failed. The following example shows this in action, and will show that the google.com site is hosted in the USA.

$objIpLocationObject = new IpLocation_Ip(new IpLocation_Service_GeoIp());
$results = $objIpLocationObject->getIpLocation('66.102.9.105'); // google.com IP address
print_r($results); // Print out the results object

Rather than include all of the service class code into this post I will just include them with in the download at the end of the post. However, I thought it might be useful to go over the services I have created here. Each service has a getIpLocation() to lookup the location from the data provided and an updateData() method that will update the data needed for the lookup.

  • IpLocation_Service_CsvMaxmind Uses the Maxmind IP to location CSV file to lookup the location. Maxmind is the same site that provides the GeoIP.dat file using in the Net_GeoIP package. This is a slightly slower method than using the dat file, but it works.
  • IpLocation_Service_CsvWebhosting Webhosting are another site that provide a CSV file of IP to location. Again, this method is slightly slower as every line before the line containing the IP location is inspected and with over 100,000 lines the potential is there for this to take a long time.
  • IpLocation_Service_Mysql This class takes the CSV created by Webhosting and inserts the information into a database. Also included in this class is a createTable() method that can be used to create the IP lookup table.

One final thing we can do with this is to encapsulate the iplocation class and make a URL to location converter. This essentially just converts the URL into an IP address and then creates an IpLocation_Ip object to do the lookup.

class IpLocation_Url
{
    /**
     * @var string The last IP address converted.
     */
    public $domain;
 
    /**
     *
     * @var string The IP address being looke up.
     */
    public $ip;
 
    /**
     * @var IpLocation_Service_Abstract The location service to use when
     *                                  converting IP addresses into locations.
     */
    private $_ipLocationService;
 
    /**
     * IpLocation_Url
     *
     * @param IpLocation_Service_Abstract $locationService The location service 
     *                                                     to use in this lookup.
     */
    public function IpLocation_Url(IpLocation_Service_Abstract $locationService)
    {
        $this->_ipLocationService = $locationService;
    }
 
    /**
     * Convert a URL to a IP address.
     *
     * @param string $url The URL being converted.
     *
     * @return string The IP address converted from the URL.
     */
    public function convertDomainToIp($url)
    {
        $this->domain    = $this->getDomainNameFromUrl($url);
        $this->ip        = gethostbyname($this->domain);
        return $this->ip;
    }
 
    /**
     * Get just the domain name from the URL.
     *
     * @param string $url The URL to extract the domain from.
     *
     * @return string The domain name.
     */
    public function getDomainNameFromUrl($url)
    {
        $tmp    = parse_url($url);
        $domain = $tmp['host'];
        return $domain;
    }
 
    /**
     * Get the location from a given URL using the _ipLocationService object.
     * Returns falue if no result found or URL is invalid.
     *
     * @param string $url The URL to convert.
     *
     * @return boolean|IpLocation_Results The IpLocation_Results results
     *                                    object. Returns false if no results.
     */
    public function getUrlLocation($url)
    {
        if (!filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)) {
            return false;
        };
 
        $ip = $this->convertDomainToIp($url);
 
        $objIpLocationObject = new IpLocation_Ip($this->_ipLocationService);
        return $objIpLocationObject->getIpLocation($ip);
    }
}

All this code is freely available over at github, along with unit tests, and can be downloaded as a zip package. Feel free to create your own IP lookup Service classes and add them to this library.

Comments

it is a great start indeed.
Permalink
Checkout my ip to country php script for a self-contained, fast and permanently updatable alternative. I won't say more because, from what I see on this page, you can figure it out.PS: I see tons of c$%p code every day. It's a treat seeing nice OO and properly formatted PHP scripts like those on this page.
Permalink
You could do the IPV4 address validation simply using the native PHP function like this: filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) It return the $ip value if successful and false otherwise. Just my 2 cents.
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
2 + 3 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.