Slyweb
На разработку сайта! Скидки 50%!

Zend FrameWork 2 и геотаргетинг

jQuery и CSS

Расскажу про геотаргетинг и как его встроить в приложения на Zend Framework 2. Геотаргетинг - это определение местоположения пользователя на основе его персональных данных, ip д.р. способы. В статье я буду рассказывать про геотаргетинг с использованием ip адреса и сервиса ipGeoBase - http://ipgeobase.ru/.

Что нужно для геотаргетинга.

Основную работу при определении местоположения пользователя будет выполнять класс "Geo". Его инициализация внутри действия контроллёра выглядит следующим образом:

  • Код
  • Чистый код
  • Копировать в буфер
  1.    $o = array(); // опции. необзятательно.
  2.    $o['charset'] = 'utf-8'; // нужно указать требуемую кодировку, если она отличается от windows-1251
  3.    $geo = $this->Geo($o);
  4.    $region = $geo->get_value('region', false);
  5.    $country = $geo->get_value('country', false);

        

Этот код нужно расположить в нутри нужного действия Вашего контроллёра, например:

  • Код
  • Чистый код
  • Копировать в буфер
  1.    public function indexAction()
  2.    {
  3.        
  4.        //.....
  5.        
  6.            $o = array(); // опции. необзятательно.
  7.            $o['charset'] = 'utf-8'; // нужно указать требуемую кодировку, если она отличается от windows-1251
  8.            $geo = $this->Geo($o);
  9.            $region = $geo->get_value('region', false);
10.            $country = $geo->get_value('country', false);
11.            
12.    }        

        

Класс Geo будет плагином Zend Frmework 2, для того чтобы к нему обращаться как плагину, его необходимо подключить в файле module.config.php для модуля Zend Framework, я делаю это следующим образом, подключая его вместе с другими модулями в моём приложении:

  • Код
  • Чистый код
  • Копировать в буфер
  1.    'controller_plugins' => array(
  2.        'invokables' => array(
  3.            'breadcrumbsFilter' => 'Application\Controller\Plugin\breadcrumbsFilter',
  4.            'Geo' => 'Application\Controller\Plugin\Geo\Geo',
  5.            'qtipFilter' => 'Application\Controller\Plugin\qtipFilter',
  6.            ),
  7.    ),    

        

Подключив плагин в конфигурационном файле, его необходимо правильно разместить по адресу: "Application\Controller\Plugin\Geo\Geo.php" - соответствие шаблону "Application\Controller\Plugin\Geo\Geo". Весь пример плагина Geo, подробное объяснение указанно после этого листинга.

  • Код
  • Чистый код
  • Копировать в буфер
  1.<?php
  2.namespace Application\Controller\Plugin\Geo;
  3.
  4.use Zend\Mvc\Controller\Plugin\AbstractPlugin;
  5.use Zend\Authentication\AuthenticationService;
  6.use Zend\ServiceManager\ServiceManagerAwareInterface;
  7.use Zend\ServiceManager\ServiceManager;
  8.
  9.use Zend\Db\Sql\Select;
10.use Zend\Db\Sql\Sql;
11.
12.class Geo extends AbstractPlugin implements ServiceManagerAwareInterface
13.
14. {
15. public function __construct($options = null) {
16.
17. $this->dirname = dirname(__file__);
18. //var_dump($options['charset']);
19. // ip
20. if(!isset($options['ip']) OR !$this->is_valid_ip($options['ip']))
21. $this->ip = $this->get_ip();
22. elseif($this->is_valid_ip($options['ip']))
23. $this->ip = $options['ip'];
24. // кодировка
25. if(isset($options['charset']) && $options['charset'] && $options['charset']!='windows-1251')
26. $this->charset = $options['charset'];
27. }
28.
29.
30. /**
31. * функция возвращет конкретное значение из полученного массива данных по ip
32. * @param string - ключ массива. Если интересует конкретное значение.
33. * Ключ может быть равным 'inetnum', 'country', 'city', 'region', 'district', 'lat', 'lng'
34. * @param bolean - устанавливаем хранить данные в куки или нет
35. * Если true, то в куки будут записаны данные по ip и повторные запросы на ipgeobase происходить не будут.
36. * Если false, то данные постоянно будут запрашиваться с ipgeobase
37. * @return array OR string - дополнительно читайте комментарии внутри функции.
38. */
39. function get_value($key = false, $cookie = true)
40. {
41. $key_array = array('inetnum', 'country', 'city', 'region', 'district', 'lat', 'lng');
42. if(!in_array($key, $key_array))
43. $key = false;
44.
45. // если используем куки и параметр уже получен, то достаем и возвращаем данные из куки
46. if($cookie && isset($_COOKIE['geobase']))
47. {
48. $data = unserialize($_COOKIE['geobase']);
49.
50. }
51. else
52. {
53. $data = $this->get_geobase_data();
54. setcookie('geobase', serialize($data), time()+3600*24*7); //устанавливаем куки на неделю
55. }
56. if($key)
57. return $data[$key]; // если указан ключ, возвращаем строку с нужными данными
58. else
59. return $data; // иначе возвращаем массив со всеми данными
60. }
61.
62. /**
63. * функция получает данные по ip.
64. * @return array - возвращает массив с данными
65. */
66. function get_geobase_data()
67. {
68.
69. $ra1 = sprintf("%u", ip2long($this->ip));
70.
71. $sql = new Sql($this->getpgAdapter());
72. $sender = $id;
73.
74. $select = new Select();
75.
76. $select->from(array('g'=>'geo_new'))
77. ->columns(array('g.city_id'=>'city_id','ip2','ip1','country'))
78. ->join(array('m'=>'master_new'),'g.city_id=m.city_id')
79. ->where("('".$ra1."')>=ip1 AND ('".$ra1."')<=ip2")
80. ->limit(1);
81.
82. $statement = $sql->prepareStatementForSqlObject($select);
83. $data = $statement->execute()->current();
84. if(empty($data)) {
85.
86. // получаем данные по ip
87. $link = 'ipgeobase.ru:7020/geo?ip='.$this->ip;
88. $ch = curl_init();
89. curl_setopt($ch, CURLOPT_URL, $link);
90. curl_setopt($ch, CURLOPT_HEADER, false);
91. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
92. curl_setopt($ch, CURLOPT_TIMEOUT, 3);
93. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
94. $string = curl_exec($ch);
95.
96. // если указана кодировка отличная от windows-1251, изменяем кодировку
97.
98. $string = iconv("windows-1251", "UTF-8", $string);
99.
100.
101. $data = $this->parse_string($string);
102. }
103. return $data;
104. }
105.
106. /**
107. * функция парсит полученные в XML данные в случае, если на сервере не установлено расширение Simplexml
108. * @return array - возвращает массив с данными
109. */
110.
111. function parse_string($string)
112. {
113. $pa['inetnum'] = '#<inetnum>(.*)</inetnum>#is';
114. $pa['country'] = '#<country>(.*)</country>#is';
115. $pa['city'] = '#<city>(.*)</city>#is';
116. $pa['region'] = '#<region>(.*)</region>#is';
117. $pa['district'] = '#<district>(.*)</district>#is';
118. $pa['lat'] = '#<lat>(.*)</lat>#is';
119. $pa['lng'] = '#<lng>(.*)</lng>#is';
120. $data = array();
121. foreach($pa as $key => $pattern)
122. {
123. preg_match($pattern, $string, $out);
124. if(isset($out[1]) && $out[1])
125. $data[$key] = trim($out[1]);
126. }
127. return $data;
128. }
129.
130. /**
131. * функция определяет ip адрес по глобальному массиву $_SERVER
132. * ip адреса проверяются начиная с приоритетного, для определения возможного использования прокси
133. * @return ip-адрес
134. */
135. function get_ip()
136. {
137. $ip = false;
138. if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
139. $ipa[] = trim(strtok($_SERVER['HTTP_X_FORWARDED_FOR'], ','));
140.
141. if (isset($_SERVER['HTTP_CLIENT_IP']))
142. $ipa[] = $_SERVER['HTTP_CLIENT_IP'];
143.
144. if (isset($_SERVER['REMOTE_ADDR']))
145. $ipa[] = $_SERVER['REMOTE_ADDR'];
146.
147. if (isset($_SERVER['HTTP_X_REAL_IP']))
148. $ipa[] = $_SERVER['HTTP_X_REAL_IP'];
149.
150. // проверяем ip-адреса на валидность начиная с приоритетного.
151. foreach($ipa as $ips)
152. {
153. // если ip валидный обрываем цикл, назначаем ip адрес и возвращаем его
154. if($this->is_valid_ip($ips))
155. {
156. $ip = $ips;
157. break;
158. }
159. }
160. return $ip;
161.
162. }
163.
164. /**
165. * функция для проверки валидности ip адреса
166. * @param ip адрес в формате 1.2.3.4
167. * @return bolean : true - если ip валидный, иначе false
168. */
169. function is_valid_ip($ip=null)
170. {
171. if(preg_match("#^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$#", $ip))
172. return true; // если ip-адрес попадает под регулярное выражение, возвращаем true
173.
174. return false; // иначе возвращаем false
175. }
176.
177. /**
178. * Retrieve service manager instance
179. *
180. * @return ServiceManager
181. */
182. public function getServiceManager()
183. {
184. return $this->serviceManager->getServiceLocator();
185. }
186.
187. /**
188. * Set service manager instance
189. *
190. * @param ServiceManager $locator
191. * @return void
192. */
193. public function setServiceManager(ServiceManager $serviceManager)
194. {
195. $this->serviceManager = $serviceManager;
196. }
197.
198. public function getpgAdapter()
199. {
200. if (!$this->pgadapter) {
201. $this->pgadapter = $this->serviceManager->getServiceLocator()->get('app_zend_db_adapter');
202. }
203.
204. return $this->pgadapter;
205. }
206.
207. }
208.?>

        

Основные функции объясняются подробно в комментариях, однако подробно объясню отдельно функцию "getpgAdapter()". Функция создаёт экземпляр класса подключения к базе данных (Adapter), создаётся он на основе конфигурационных данных содержащихся в файлах, как правило это файл "config\autoload\global.php", согласно документации:

"It is important to know that by using this style of adapter creation, the Adapter will attempt to create any dependencies that were not explicitly provided. A Driver object will be created from the configuration array provided in the constructor. A Platform object will be created based off the type of Driver object that was instantiated. And lastly, a default ResultSet object is created and utilized. Any of these objects can be injected, to do this, see the next section."

Ещё хотелось объяснить интересную функцию "get_geobase_data", по сути это модифицирлваннфый вариант скрипта скаченного с сайта ipgeobase, за исключением того, что мы помещаем данные в базу (о чём будет рассказано после следующего листинга), а после этого используем их для наших целей, согласитесь, поиск по базе данных с индексированным столбцами намного быстрее и удобнее чем работа с xlm данными, хотя скрипт написан таким образом, что когда в базе не находится нужный регион, происходит обращение к xml файлу с официального сайта - ipgeobase, с надеждой найти данные там.

  • Код
  • Чистый код
  • Копировать в буфер
  1.function get_geobase_data()
  2.        {
  3.            $ra1 = sprintf("%u", ip2long($this->ip));
  4.            $sql = new Sql($this->getpgAdapter());
  5.            $sender = $id;
  6.            $select = new Select();
  7.            
  8.            $select->from(array('g'=>'geo_new'))
  9.            ->columns(array('g.city_id'=>'city_id','ip2','ip1'))
10.            ->join(array('m'=>'master_new'),'g.city_id=m.city_id')
11.            ->where("('".$ra1."')>=ip1 AND ('".$ra1."')<=ip2")    
12.            ->limit(1);
13.            $statement = $sql->prepareStatementForSqlObject($select);
14.            $results = $statement->execute()->current();
15.            $data['region'] = $results['region'];
16.            
17.            if(!$results['region']) {
18.             // получаем данные по ip
19.                $link = 'ipgeobase.ru:7020/geo?ip='.$this->ip;
20.                $ch = curl_init();
21.                curl_setopt($ch, CURLOPT_URL, $link);
22.                curl_setopt($ch, CURLOPT_HEADER, false);
23.                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
24.                curl_setopt($ch, CURLOPT_TIMEOUT, 3);
25.                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
26.                $string = curl_exec($ch);    
27.                
28.             // если указана кодировка отличная от windows-1251, изменяем кодировку
29.                    
30.                $string = iconv("windows-1251", "UTF-8", $string);
31.                
32.                        
33.                $data = $this->parse_string($string);
34.            }
35.            return $data;
36.        }    

        

Как я и обещал, ниже приведён файл который может использоваться для обновления данных содержащихся в базе, поместите этот файл в директорию, скрытыю от публичного доступа.

  • Код
  • Чистый код
  • Копировать в буфер
  1.<?php
  2.$master_file = 'cities.txt'; // Наши основной файл с ip
  3.$slave_file = 'cidr_optim.txt'; // Наши дополнительный файл с ip
  4.include '../config.php';
  5.
  6.class getXmltoDb {
  7.
  8.    protected $Mymysql;    
  9.        
10.    function __construct(){
11.        $connection = mysql_connect ("localhost", USER, PASS)
12.        or die ("Ошибка соединения с сервером");
13.        $db = mysql_select_db (DBS, $connection)
14.        or die ("Ошибка при выборе базы данных");
15.        
16.        mysql_query("SET NAMES 'utf8'"); // кодировка для соединения с базой данных
17.        mysql_query("SET CHARACTER SET 'utf8'"); // кодировка для соединения с базой данных
18.        mysql_query("SET SESSION wait_timeout = 2800"); // кодировка для соединения с базой данных
19.        mysql_query("TRUNCATE `geo_new`;"); // кодировка для соединения с базой данных
20.        mysql_query("TRUNCATE `master_new`;"); // кодировка для соединения с базой данных
21.        set_time_limit(10000);
22.    }
23.    
24.    function my_ip_slave ($slave_file) {
25.        $slave_fp = file($slave_file); // Читаем строки файла в массив
26.
27.        foreach ($slave_fp as $value) {
28.                $cell = explode("\t", iconv("windows-1251", "UTF-8", $value));// Разбиваем каждую строку на массив
29.                $query_slave = "INSERT INTO `geo_new` (`ip1`, `ip2`, `blockip`, `country`, `city_id`) VALUES
30.                                ('$cell[0]','$cell[1]','$cell[2]','$cell[3]', '$cell[4]');";
31.                $result = mysql_query ($query_slave) // выполняем запрос и заполняем базу данных ip адресами
32.                or die ("Ошибка при выполнении запроса: ".mysql_error ());
33.        
34.        
35.        }
36.
37.    
38.    }
39.
40.    function my_ip_master($master_file) {
41.        $slave_fp = file($master_file); // Читаем строки файла в массив
42.
43.        foreach ($slave_fp as $value) {
44.        
45.            $cell = explode("\t", iconv("windows-1251", "UTF-8", $value));// Разбиваем каждую строку на массив
46.            $query = "INSERT INTO master_new (city_id,city, region, okrug, coord1,coord2) VALUES
47.            ('$cell[0]','$cell[1]','$cell[2]', '$cell[3]', '$cell[4]', '$cell[5]');";
48.            $result = mysql_query ($query) // выполняем запрос и заполняем базу данных ip адресами
49.            or die ("Ошибка при выполнении запроса: ".mysql_error ());
50.        }
51.
52.    }
53.    
54.    function grabGeoIpBase(){
55.    
56.        $link = 'http://ipgeobase.ru/files/db/Main/geo_files.zip';
57.        $ch = curl_init();
58.        curl_setopt($ch, CURLOPT_URL, $link);
59.        curl_setopt($ch, CURLOPT_HEADER, false);
60.        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
61.        curl_setopt($ch, CURLOPT_TIMEOUT, 3);
62.        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
63.        $string = curl_exec($ch);
64.
65.        $fp = fopen('geo.zip','w');
66.        fwrite($fp,$string);
67.        fclose($fp);
68.    
69.
70.
71.        $zip = new ZipArchive;
72.
73.        if ($zip->open('geo.zip') === TRUE) {
74.            $zip->extractTo('./');
75.            $zip->close();
76.            return true;
77.        } else {
78.            return false;
79.        }
80.
81.    
82.    }
83.    
84.    
85.    
86.}
87.
88.$parser = new getXmltoDb();
89.// поехали!
90.if($parser->grabGeoIpBase()) {
91.
92.    $parser->my_ip_master($master_file);
93.    $parser->my_ip_slave($slave_file);
94.}
95.
96.
97.
98.?>
99.    

        

Config.php

  • Код
  • Чистый код
  • Копировать в буфер
  1.<?php
  2.define('DBS','');
  3.define('USER','');
  4.define('PASS','');
  5.?>

        

После этого осталось создать запись в файле crontab "05 04 * * * /usr/bin/php /var/www/jquery/data/parser/index.php >/dev/null 2>&1"

Полученные данные в нашем действии, о котором мы писали ранее (indexAction), будут содержаться в переменных $region и country. В примере выполняются два запроса, в действительности хватает одного запроса через функцию get_value('', false), которая будет содержать массив данных.

Скачать архив парсера xml в базу данных.


Александр Ермаков