In this tutorial, we’ll show you how to get geolocation from a visitor’s IP using the Free IP2Location™ LITE Database in Symfony 5 project. This project supports for Symfony version 4 onward.
Step 1. Download Free IP2Location™ LITE Database
As the first step, you will need the CSV database of IP2Location in your Symfony 5 project. We recommend starting with the IP2Location™ DB11 LITE IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE Database which is free! Unzip the database file and import it into your MySQL table after you have downloaded it from https://lite.ip2location.com/database/db11-ip-country-region-city-latitude-longitude-zipcode-timezone.
1.Create ‘ip2location_db11’ table.
CREATE DATABASE ip2location; USE ip2location; CREATE TABLE `ip2location_db11`( `ip_from` INT(10) UNSIGNED, `ip_to` INT(10) UNSIGNED, `country_code` CHAR(2), `country_name` VARCHAR(64), `region_name` VARCHAR(128), `city_name` VARCHAR(128), `latitude` DOUBLE, `longitude` DOUBLE, `zip_code` VARCHAR(30), `time_zone` VARCHAR(8), INDEX `idx_ip_from` (`ip_from`), INDEX `idx_ip_to` (`ip_to`), INDEX `idx_ip_from_to` (`ip_from`, `ip_to`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
2. Import the data into table ‘ip2location_db11’.
LOAD DATA LOCAL INFILE 'IP2LOCATION-LITE-DB11.CSV' INTO TABLE `ip2location_db11` FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\r\n' IGNORE 0 LINES;
Step 2. Create a Symfony project
Run the following command to create a Symfony 4 project.> composer create-project symfony/website-skeleton <projectname>
Step 3. Creating routes & twig
Run these commands once in your project to add support.> composer require annotations
> composer require twig
Step 4. Configure env. file
Open your env. file and configure the database user, password and name in at this line.
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
Step 5. Change the following code.
Since our database has no primary key, you need to change some code in the following files.
1. Copy & paste the following codes in /vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php file.
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException;
/**
* The DatabaseDriver reverse engineers the mapping metadata from a database.
*
* @link www.doctrine-project.org
* @since 2.0
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class DatabaseDriver implements MappingDriver
{
/**
* @var AbstractSchemaManager
*/
private $_sm;
/**
* @var array|null
*/
private $tables = null;
/**
* @var array
*/
private $classToTableNames = [];
/**
* @var array
*/
private $manyToManyTables = [];
/**
* @var array
*/
private $classNamesForTables = [];
/**
* @var array
*/
private $fieldNamesForColumns = [];
/**
* The namespace for the generated entities.
*
* @var string|null
*/
private $namespace;
/**
* @param AbstractSchemaManager $schemaManager
*/
public function __construct(AbstractSchemaManager $schemaManager)
{
$this->_sm = $schemaManager;
}
/**
* Set the namespace for the generated entities.
*
* @param string $namespace
*
* @return void
*/
public function setNamespace($namespace)
{
$this->namespace = $namespace;
}
/**
* {@inheritDoc}
*/
public function isTransient($className)
{
return true;
}
/**
* {@inheritDoc}
*/
public function getAllClassNames()
{
$this->reverseEngineerMappingFromDatabase();
return array_keys($this->classToTableNames);
}
/**
* Sets class name for a table.
*
* @param string $tableName
* @param string $className
*
* @return void
*/
public function setClassNameForTable($tableName, $className)
{
$this->classNamesForTables[$tableName] = $className;
}
/**
* Sets field name for a column on a specific table.
*
* @param string $tableName
* @param string $columnName
* @param string $fieldName
*
* @return void
*/
public function setFieldNameForColumn($tableName, $columnName, $fieldName)
{
$this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
}
/**
* Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
*
* @param array $entityTables
* @param array $manyToManyTables
*
* @return void
*/
public function setTables($entityTables, $manyToManyTables)
{
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($entityTables as $table) {
$className = $this->getClassNameForTable($table->getName());
$this->classToTableNames[$className] = $table->getName();
$this->tables[$table->getName()] = $table;
}
foreach ($manyToManyTables as $table) {
$this->manyToManyTables[$table->getName()] = $table;
}
}
/**
* {@inheritDoc}
*/
public function loadMetadataForClass($className, ClassMetadata $metadata)
{
$this->reverseEngineerMappingFromDatabase();
if ( ! isset($this->classToTableNames[$className])) {
throw new \InvalidArgumentException("Unknown class " . $className);
}
$tableName = $this->classToTableNames[$className];
$metadata->name = $className;
$metadata->table['name'] = $tableName;
$this->buildIndexes($metadata);
$this->buildFieldMappings($metadata);
$this->buildToOneAssociationMappings($metadata);
foreach ($this->manyToManyTables as $manyTable) {
foreach ($manyTable->getForeignKeys() as $foreignKey) {
// foreign key maps to the table of the current entity, many to many association probably exists
if ( ! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) {
continue;
}
$myFk = $foreignKey;
$otherFk = null;
foreach ($manyTable->getForeignKeys() as $foreignKey) {
if ($foreignKey != $myFk) {
$otherFk = $foreignKey;
break;
}
}
if ( ! $otherFk) {
// the definition of this many to many table does not contain
// enough foreign key information to continue reverse engineering.
continue;
}
$localColumn = current($myFk->getColumns());
$associationMapping = [];
$associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true);
$associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName());
if (current($manyTable->getColumns())->getName() == $localColumn) {
$associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
$associationMapping['joinTable'] = [
'name' => strtolower($manyTable->getName()),
'joinColumns' => [],
'inverseJoinColumns' => [],
];
$fkCols = $myFk->getForeignColumns();
$cols = $myFk->getColumns();
for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
$associationMapping['joinTable']['joinColumns'][] = [
'name' => $cols[$i],
'referencedColumnName' => $fkCols[$i],
];
}
$fkCols = $otherFk->getForeignColumns();
$cols = $otherFk->getColumns();
for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
$associationMapping['joinTable']['inverseJoinColumns'][] = [
'name' => $cols[$i],
'referencedColumnName' => $fkCols[$i],
];
}
} else {
$associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true);
}
$metadata->mapManyToMany($associationMapping);
break;
}
}
}
/**
* @return void
*
* @throws \Doctrine\ORM\Mapping\MappingException
*/
private function reverseEngineerMappingFromDatabase()
{
if ($this->tables !== null) {
return;
}
$tables = [];
foreach ($this->_sm->listTableNames() as $tableName) {
$tables[$tableName] = $this->_sm->listTableDetails($tableName);
}
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($tables as $tableName => $table) {
$foreignKeys = ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints())
? $table->getForeignKeys()
: [];
$allForeignKeyColumns = [];
foreach ($foreignKeys as $foreignKey) {
$allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns());
}
// if ( ! $table->hasPrimaryKey()) {
// throw new MappingException(
// "Table " . $table->getName() . " has no primary key. Doctrine does not ".
// "support reverse engineering from tables that don't have a primary key."
// );
// }
$pkColumns = [];
if($table->hasPrimaryKey()){
$pkColumns = $table->getPrimaryKey()->getCoumns();
sort($pkColumns);
}
sort($allForeignKeyColumns);
if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) {
$this->manyToManyTables[$tableName] = $table;
} else {
// lower-casing is necessary because of Oracle Uppercase Tablenames,
// assumption is lower-case + underscore separated.
$className = $this->getClassNameForTable($tableName);
$this->tables[$tableName] = $table;
$this->classToTableNames[$className] = $tableName;
}
}
}
/**
* Build indexes from a class metadata.
*
* @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
*/
private function buildIndexes(ClassMetadataInfo $metadata)
{
$tableName = $metadata->table['name'];
$indexes = $this->tables[$tableName]->getIndexes();
foreach ($indexes as $index) {
if ($index->isPrimary()) {
continue;
}
$indexName = $index->getName();
$indexColumns = $index->getColumns();
$constraintType = $index->isUnique()
? 'uniqueConstraints'
: 'indexes';
$metadata->table[$constraintType][$indexName]['columns'] = $indexColumns;
}
}
/**
* Build field mapping from class metadata.
*
* @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
*/
private function buildFieldMappings(ClassMetadataInfo $metadata)
{
$tableName = $metadata->table['name'];
$columns = $this->tables[$tableName]->getColumns();
$primaryKeys = $this->hasTablePrimaryKeys($this->tables[$tableName]);
$foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
$allForeignKeys = [];
foreach ($foreignKeys as $foreignKey) {
$allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns());
}
$ids = [];
$fieldMappings = [];
foreach ($columns as $column) {
if (in_array($column->getName(), $allForeignKeys)) {
continue;
}
$fieldMapping = $this->buildFieldMapping($tableName, $column);
if ($primaryKeys && in_array($column->getName(), $primaryKeys)) {
$fieldMapping['id'] = true;
$ids[] = $fieldMapping;
}
$fieldMappings[] = $fieldMapping;
}
// We need to check for the columns here, because we might have associations as id as well.
if ($ids && count($primaryKeys) == 1) {
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
}
foreach ($fieldMappings as $fieldMapping) {
$metadata->mapField($fieldMapping);
}
}
/**
* Build field mapping from a schema column definition
*
* @param string $tableName
* @param \Doctrine\DBAL\Schema\Column $column
*
* @return array
*/
private function buildFieldMapping($tableName, Column $column)
{
$fieldMapping = [
'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false),
'columnName' => $column->getName(),
'type' => $column->getType()->getName(),
'nullable' => ( ! $column->getNotnull()),
];
// Type specific elements
switch ($fieldMapping['type']) {
case Type::TARRAY:
case Type::BLOB:
case Type::GUID:
case Type::JSON_ARRAY:
case Type::OBJECT:
case Type::SIMPLE_ARRAY:
case Type::STRING:
case Type::TEXT:
$fieldMapping['length'] = $column->getLength();
$fieldMapping['options']['fixed'] = $column->getFixed();
break;
case Type::DECIMAL:
case Type::FLOAT:
$fieldMapping['precision'] = $column->getPrecision();
$fieldMapping['scale'] = $column->getScale();
break;
case Type::INTEGER:
case Type::BIGINT:
case Type::SMALLINT:
$fieldMapping['options']['unsigned'] = $column->getUnsigned();
break;
}
// Comment
if (($comment = $column->getComment()) !== null) {
$fieldMapping['options']['comment'] = $comment;
}
// Default
if (($default = $column->getDefault()) !== null) {
$fieldMapping['options']['default'] = $default;
}
return $fieldMapping;
}
/**
* Build to one (one to one, many to one) association mapping from class metadata.
*
* @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
*/
private function buildToOneAssociationMappings(ClassMetadataInfo $metadata)
{
$tableName = $metadata->table['name'];
$primaryKeys = $this->hasTablePrimaryKeys($this->tables[$tableName]);
$foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]);
foreach ($foreignKeys as $foreignKey) {
$foreignTableName = $foreignKey->getForeignTableName();
$fkColumns = $foreignKey->getColumns();
$fkForeignColumns = $foreignKey->getForeignColumns();
$localColumn = current($fkColumns);
$associationMapping = [
'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true),
'targetEntity' => $this->getClassNameForTable($foreignTableName),
];
if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) {
$associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
}
if ($primaryKeys && in_array($localColumn, $primaryKeys)) {
$associationMapping['id'] = true;
}
for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) {
$associationMapping['joinColumns'][] = [
'name' => $fkColumns[$i],
'referencedColumnName' => $fkForeignColumns[$i],
];
}
// Here we need to check if $fkColumns are the same as $primaryKeys
if ( ! array_diff($fkColumns, $primaryKeys)) {
$metadata->mapOneToOne($associationMapping);
} else {
$metadata->mapManyToOne($associationMapping);
}
}
}
/**
* Retrieve schema table definition foreign keys.
*
* @param \Doctrine\DBAL\Schema\Table $table
*
* @return array
*/
private function getTableForeignKeys(Table $table)
{
return ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints())
? $table->getForeignKeys()
: [];
}
/**
* Retrieve schema table definition primary keys.
*
* @param \Doctrine\DBAL\Schema\Table $table
*
* @return array
*/
private function hasTablePrimaryKeys(Table $table)
{
try {
return ($table->hasPrimaryKey())?$table->getPrimaryKey()->getColumns():array();
} catch (SchemaException $e) {
// Do nothing
}
return [];
}
/**
* Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
*
* @param string $tableName
*
* @return string
*/
private function getClassNameForTable($tableName)
{
if (isset($this->classNamesForTables[$tableName])) {
return $this->namespace . $this->classNamesForTables[$tableName];
}
return $this->namespace . Inflector::classify(strtolower($tableName));
}
/**
* Return the mapped field name for a column, if it exists. Otherwise return camelized version.
*
* @param string $tableName
* @param string $columnName
* @param boolean $fk Whether the column is a foreignkey or not.
*
* @return string
*/
private function getFieldNameForColumn($tableName, $columnName, $fk = false)
{
if (isset($this->fieldNamesForColumns[$tableName]) && isset($this->fieldNamesForColumns[$tableName][$columnName])) {
return $this->fieldNamesForColumns[$tableName][$columnName];
}
$columnName = strtolower($columnName);
// Replace _id if it is a foreignkey column
if ($fk) {
$columnName = str_replace('_id', '', $columnName);
}
return Inflector::camelize($columnName);
}
}
2. Find the function identifierRequired($entityName) and comment on the following codes in /vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/MappingException.php file.
// /**
// * @param string $entityName
// *
// * @return MappingException
// */
// public static function identifierRequired($entityName)
// {
// if (false !== ($parent = get_parent_class($entityName))) {
// return new self(sprintf(
// 'No identifier/primary key specified for Entity "%s" sub class of "%s". Every Entity must have an identifier/primary key.',
// $entityName, $parent
// ));
// }
// return new self(sprintf(
// 'No identifier/primary key specified for Entity "%s". Every Entity must have an identifier/primary key.',
// $entityName
// ));
// }
3. Find and comment on the following codes in /vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php file.
// // Verify & complete identifier mapping
// if ( ! $this->identifier) {
// throw MappingException::identifierRequired($this->name);
// }
Step 6. Import database & generate getter and setter methods.
Run these commands to import the database and generate getter and setter.> php bin/console doctrine:mapping:import "App/Entity" annotations --path=src/Entity
> php bin/console make:entity --regenerate App
Sep 7. Below are the sample codes.
DefaultController.php
<?php
namespace App\Controller;
use App\Entity\Ip2location;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\Ip2locationRepository;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/", name="ip2location.")
*/
class IPController extends AbstractController
{
/**
* @Route("/home", methods={"GET", "HEAD"})
* @param Ip2locationRepository $rep
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(Ip2locationRepository $rep)
{
$Ip2location = new Ip2location();
return $this->render('ip/index.html.twig',[]);
}
//Create a search form to search the location of the user by their IP address.
public function searchBar()
{
$form = $this->createFormBuilder()
->setAction($this->generateUrl('ip2location.result'))
->add('ip', TextType::class, [
'label' => 'IP Address: '
])
->add('submit', SubmitType::class, [
'attr' => [
'class' => 'btn btn-primary'
]
])
->getForm();
return $this->render('ip/searchBar.html.twig', [
'form' => $form->createView(),
]);
}
//Display the location result
/**
* @Route("/result", name="result")
* @param Request $request
*/
public function result(Request $request, Ip2locationRepository $rep)
{
$query = $request->request->get('form')['ip'];
if($query){
$ip = $rep->Dot2LongIP($query);
$ipnum = $rep->findByIP($ip);
}
return $this->render('ip/show.html.twig', [
'ipnum' => $ipnum,
'ip' => $query
]);
}
}
Repository.php
//SQL query string to match the recordset that the IP number fall between the valid range
public function findByIP($ipaddress)
{
$conn = $this->getEntityManager()->getConnection();
$sql = '
SELECT *
FROM ip2location i
WHERE :ip <= i.ip_to LIMIT 1';
$stmt = $conn->prepare($sql);
$stmt->execute(['ip' => $ipaddress]);
return $stmt->fetchAll();
}
//Function to convert IP address (xxx.xxx.xxx.xxx) to IP number (0 to 256^4-1)
public function Dot2LongIP($ipaddr)
{
if($ipaddr == ""){
return 0;
}
else {
$ips = explode(".", $ipaddr);
return ($ips[3] + $ips[2] * 256 + $ips[1] * 256 * 256 + $ips[0] * 256 * 256 * 256);
}
}
searchBar.html.twig
{{ form(form) }}
show.html.twig
<h3>Your Location</h3>
<p>IP Address: {{ ip }} </p>
{% for key in ipnum %}
<div>
<p>Country Code: {{ key.country_code }} </p>
<p>Country Name: {{ key.country_name }} </p>
<p>Region Name: {{ key.region_name }} </p>
<p>City Name: {{ key.city_name }} </p>
<p>Latitude: {{ key.latitude }} </p>
<p>Longitude: {{ key.longitude }} </p>
<p>Zip Code: {{ key.zip_code }} </p>
<p>Time Zone: {{ key.time_zone }} </p>
</div>
{% endfor %}
base.html.twig
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
{% block stylesheets %}{% endblock %}
</head>
<body>
<div class="container">
{{ render(controller(
'App\\Controller\\IPController::searchBar'
))}}
{% block body %}{% endblock %}
</div>
{% block javascripts %}{% endblock %}
</body>
</html>

