БлогNot. Ещё одна голосовалка на JQuery+PHP :)

Ещё одна голосовалка на JQuery+PHP :)

В основном, в учебных целях, я не утверждаю, что улучшать нечего. Скрипт представляет собой простое голосование, связанное с выбором одного варианта ответа из нескольких предложенных.

Смысл использования JQuery - возможность встроить голосование не в отдельный HTML-фрейм или устанавливаемый на сервер скрипт, а непосредственно в HTML-содержимое. Тогда, зная id опроса и адрес скрипта, к которому он обращается, можно разместить форму на произвольном числе страниц (одного домена, конечно). Ну и при любом заходе юзверьков на эти страницы они будут видеть актуальное состояние опроса :)

Вот "голосование по умолчанию" непосредственно в работе (в новой вкладке, а не встроено в ленту, т.к. скрипты лежат в другом домене и вызываю без фрейма).

Обратите внимание, что когда вы проголосуете, страничка, на которую выведен опрос, всё равно перезагрузится - мы же отправляли данные на сервер методом POST.

Дело в том, что Javascript (клиентский) в принципе не может обратиться к MySQL (серверному). Серверный Javascript, типа NodeJS, может, но он должен быть установлен на сервере, а не формироваться на страничке в вашем браузере.

Мы можем только обратиться к серверному скрипту (например, написанному на PHP) и передать ему данные, а он уже сделает с ними то, что нужно.

Код файла с примером вызова скрипта (index.html):

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
</head>
<body> 
<p>Здесь любое содержимое.

<div id="responsecontainer"></div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
 $.ajax({
  type: "POST",
  url: "display.php",
  data: { id:1 },
  dataType: "html",
  success: function(response) {
   $("#responsecontainer").html(response);
  },
  error: function() {
   $("#responsecontainer").html("<p>Ошибка получения данных с сервера через AJAX!</p>");
  }
 });
});
</script>
<noscript><p>Извините, для работы скрипта нужен включённый в браузере Javascript!</p></noscript>

<p>Здесь тоже любое содержимое.</p>
</body></html>

Файл jquery.min.js берётся с с сервера Гугля, а в остальном ajax-код соверешенно типовой.

Для вывода блока с опросом используется контейнер с именем responsecontainer.

Вместо id:1 можно передать id другого опроса, если я таковой создам в базе.

Поговорим о структуре данных.

Мне было нужно, чтобы "накрутки" повторным голосованием были минимальными, то есть, скрипт определяет и сохраняет в базе данных IP-адреса проголосовавших и, на всякий случай, метку времени голосования. Это уже одна таблица в MySQL, пусть назвается jqueryvotingresults.

Для хранения вариантов одного голосования я хотел использовать только одну запись базы данных, экономя на чтении, так что варианты вопросов и количества проголосовавших за каждый вариант элементарно разделяются переводом строки. Считать каждый раз SQL-запросами, по какому варианту сколько отдано голосов, считаю лишним. Но раз голосований может быть сколько угодно, понадобится отдельная таблица для них, назовём её jqueryvoting.

В соответствии со сказанным, вот SQL-запрос, создающий таблицы данных с одной предустановленной записью об "опросе по умолчанию" с id=1:

DROP TABLE IF EXISTS jqueryvoting;
CREATE TABLE IF NOT EXISTS jqueryvoting (
  id int PRIMARY KEY,
  name varchar(80),
  data text,
  results text
);
DROP TABLE IF EXISTS jqueryvotingresults;
CREATE TABLE IF NOT EXISTS jqueryvotingresults (
 n int PRIMARY KEY auto_increment,
 id int,
 ip varchar(15),
 var int,
 date bigint
);
insert into jqueryvoting (id, name, data, results) values (
 "1", "Как вам такой опрос?", 
 "Угу, просто\nНеа, слишком просто\nНи фига не понял", 
 "0\n0\n0"
);

Как всегда, это можно выполнить в phpMyAdmin. Его современные версии разрешают редактировать таблицы, только если в них есть первичный ключ, так что сделаем таковые.

Кроме того, хотелось посмотреть, можно ли сделать так, чтобы ajax нормально возвращал данные из базы, хранящиеся в кодировке windows-1251. Это оказалось вполне возможно, если правильно указать тег кодировки в документе (см. index.html), хранить данные в базе в той же кодировке, указать её же для MySQLi (см. функцию get_conid в functions.php) и, наконец, не забыть отправить соответствующий заголовок перед тем, как возвращать содержимое из PHP-скрипта, вызванного со стороны клиента через Ajax (см. вызов header в верхней части display.php).

Всю работу выполняет как раз display.php - смотрит, есть ли в базе опрос с запрошенным id, а также есть ли уже для IP-адреса пользователя запись о голосовании. Если да - показывает результаты опроса на момент загрузки страницы, если нет - предлагает проголосовать.

<?php
 require_once 'functions.php';
 header('Content-type: text/html; charset=windows-1251');
 $ip = get_ip();
 $value = '';
 if (!isset($_POST['id'])) {
  $value = 'Не передан ID голосования';
 }
 else {
  $id = intval($_POST['id']);
  if ($id<1) {
   $value = 'Неверный ID голосования';
  }
  else {
   $result1 = dbquery('select * from '.DB_DATA.' where id="'.$id.'" limit 0,1');
   if (!$result1) {
    $value = 'Неверный запрос 1: '.mysql_error();
   }
   else {
    if (!dbrows($result1)) {
     $value = 'Не найдено данных для голосования с ID='.$id;
    }
    else {
     $note = dbfetcha($result1);
     $data = explode("\n",$note['data']);
     $results = explode("\n",$note['results']);
     if (count($data)<2 or count($results)<2 or count($data)!=count($results)) {
      $value = 'Неверные данные в таблице '.DB_DATA.' для записи '.$note['id'].':<br>'.
       $note['data'].'<br>и'.$note['results'];
     }
     else {
      $result2 = dbquery('select * from '.DB_RESULTS.' where ip="'.$ip.'" and id="'.$id.'" limit 0,1');
      if (!$result2) {
       $value = 'Неверный запрос 2: '.mysql_error();
      }
      else {
       if (!dbrows($result2)) {
        $value = '<form method="post" action="'.get_server_url().
         'vote.php" name="jqueryvotingform"><p><b>'.$note['name'].'</b></p>'."\n";
        $value .= '<input type="hidden" name="id" value="'.$id.'">'."\n";
        for ($i=0; $i<count($data); $i++) {
         $value .= '<p><input name="q" id="r'.($i+1).'" value="'.($i+1).'" type="radio"> '.
          '<label for="r'.($i+1).'">'.$data[$i].'</label></p>';
        }
        $value .= '<input type="submit" value="Ответить"></form>';
       }
       else {
        $value = '<p><b>'.$note['name'].'</b></p>';
        $all = array_sum($results);
        for ($i=0; $i<count($data); $i++) {
         $ps = 0;
         if ($all) $ps = round($results[$i]/$all * 100.,1);
         $value .= '<p>'.$data[$i].': <b>'.$results[$i].'</b> ('.$ps.'%)</p>';
        }
        $user = dbfetcha($result2);
        $value .= '<p><b>Ваш голос</b>: "'.$data[$user['var']-1].'", '.
         date("d.m.Y, H:i",$user['date']).'. Всего голосов: <b>'.$all.'</b></p>';
       }
      }
     }
    }
   }
  }
 }
 echo $value;
?>

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

Если пользователь нажал кнопку "Ответить", в дело вступает файл vote.php. Он проверит правильность полученных данных, если всё верно, добавит в таблицу результатов запись о голосовании и перенаправит выполнение на ту страницу, откуда его вызвали.

Обращаю внимание, что "сделать всё внутри Ajax", без перезагрузки страницы вообще, по-моему, при таком подходе нельзя.

Представьте, что мы засунули в vote.php ещё один программно формируемый ajax-запрос, который должен отправить на сервер выбор пользователя, сделанный им при голосовании. Но выбор-то в момент загрузки страницы ещё не сделан, не выбирать же за клиента одну из радиокнопок? Нельзя вытащить самого себя за волосы из болота.

Можно было бы полностью сформировать опрос на стороне клиента с помощью Javascript и передать данные о выборе пользователя Ajax-запросом на сервер, вызванный PHP-скрипт внесёт текущие данные, снова прочитает из базы изменения и вернёт новые данные как HTML-контент. Тогда страница не будет перегружаться при голосовании, но для каждого нового опроса нужно писать новый код на Javascript, а это - совсем другая задача.

Вот полный листинг vote.php:

<?php
 require_once 'functions.php';
 $ip = get_ip();
 $value = '';
 if (isset($_POST['q']) and !empty($_POST['q'])) {
  $q = intval($_POST['q']);
  if ($q>0) {
   if (isset($_POST['id']) and !empty($_POST['id'])) {
    $id = intval($_POST['id']);
    if ($id>0) {
     $result1 = dbquery('select * from '.DB_DATA.' where id="'.$id.'" limit 0,1');
     if ($result1 and dbrows($result1)) {
      $note = dbfetcha($result1);
      $data = explode("\n",$note['data']);
      $results = explode("\n",$note['results']);
      if (count($data)>1 and count($results)>1 and count($data)==count($results)) {
       if ($q<=count($data)) {
        $result2 = dbquery('select * from '.DB_RESULTS.' where ip="'.$ip.'" and id="'.$id.'" limit 0,1');
        if ($result2) {
         if (!dbrows($result2)) {
          $result3 = dbquery('insert into '.DB_RESULTS.' (id, ip, var, date) '.
           'values ("'.$id.'", "'.$ip.'", "'.$q.'", "'.time().'")');
          if ($result3) {
           $results[$q-1] += 1;
           $new_results = implode ("\n",$results);
           $result4 = dbquery('update '.DB_DATA.' set results = "'.$new_results.'" where id = "'.$id.'"');
           if (!$result4) {
            die ('Неверный запрос 4: '.mysql_error());
           }
          }
          else {
           die ('Неверный запрос 3: '.mysql_error());
          }
         }
        }
        else {
         die ('Неверный запрос 2: '.mysql_error());
        }
       }
       else {
        die ("Неверный номер варианта $q");
       }
      }
      else {
       die ("Неверная запись опроса $id в БД");
      }
     }
     else {
      die ('Неверный запрос 1: '.mysql_error());
     }
    }
    else {
     die ("Получен неверный id=$id");
    }
   }
   else {
    die ("Не передан id");
   }
  }
  else {
   die ("Неверный номер варианта $q");
  }
 } 
 else {
  die ("Не передан q");
 }
 if (isset ($_SERVER['HTTP_REFERER'])) {
  header('Location: '.$_SERVER['HTTP_REFERER']);
 }
 else {
  header('Location: '.get_server_url());
 }
?>

Служебный файл functions.php, как обычно, содержит нужные для работы функции, мы не используем устаревающий MySQL, а перешли на MySQLi:

<?php

 require_once('config.php');

 if (!isset($conid)) {

  function dbconnect() {
   $mysql=mysqli_connect(DB_HOST, DB_USER, DB_PASS); //хост, логин, пароль
   mysqli_select_db($mysql,DB_NAME); //имя базы данных
   return $mysql;
  }

  function dbquery ($sql) { return mysqli_query(get_conid(),$sql); }

  function dbfetcha ($result) {
   if ($row=mysqli_fetch_assoc ($result)) return $row;
   else return false;
  }

  function dbfetch ($result){
   if ($row=mysqli_fetch_array($result)) return $row;
   else return false;
  }

  function dbrows ($result) { return mysqli_num_rows($result); }

  function dbfree ($result) { mysqli_free_result($result); }

  function dbclose ($conid) { mysqli_close(get_conid()); }
 
  function get_conid () {
   static $conid=0;
   if ($conid==0) {
    date_default_timezone_set (DEFAULT_TIMEZONE);
    $conid=dbconnect();
    mysqli_set_charset ($conid,DB_ENCODING);
    dbquery("SET NAMES ".DB_ENCODING,$conid);
   }
   return $conid;
  }

  get_conid ();
 }

 function get_ip () {
  if( getenv('HTTP_X_FORWARDED_FOR') != '' ) {
   $client_ip = ( !empty($_SERVER['REMOTE_ADDR']) ) ? $_SERVER['REMOTE_ADDR'] : 
    ( ( !empty($_ENV['REMOTE_ADDR']) ) ? $_ENV['REMOTE_ADDR'] : $REMOTE_ADDR );
   $entries = explode(',', getenv('HTTP_X_FORWARDED_FOR'));
   reset($entries);
   while (list(, $entry) = each($entries)) {
    $entry = trim($entry);
    if ( preg_match("/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/", $entry, $ip_list) ) {
     $private_ip = array('/^0\./', '/^127\.0\.0\.1/', '/^192\.168\..*/', 
      '/^172\.((1[6-9])|(2[0-9])|(3[0-1]))\..*/', '/^10\..*/', '/^224\..*/', '/^240\..*/');
     $found_ip = preg_replace($private_ip, $client_ip, $ip_list[1]);
     if ($client_ip != $found_ip) {
      $client_ip = $found_ip;
      break;
     }
    }
   }
  }
  else {
   $client_ip = ( !empty($_SERVER['REMOTE_ADDR']) ) ? $_SERVER['REMOTE_ADDR'] : 
    ( ( !empty($_ENV['REMOTE_ADDR']) ) ? $_ENV['REMOTE_ADDR'] : $REMOTE_ADDR );
  }
  return $client_ip;
 }

 function get_server_url () {
  $s='http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'];
  $sb=strrchr($s,"/");
  if ($sb!==false) $s=substr($s,0,-(strlen($sb)-1));
  return $s;
 }
?>

Файл настроек config.php хранит в одном месте все нужны настройки скрипта. На реальном хостинге значения его констант, разумеется, изменятся.

<?php
 define('DB_HOST', 'localhost'); //Хост, на котором размещен сервер MySQL
 define('DB_USER', 'root'); //Имя пользователя MySQL
 define('DB_PASS', ''); //Пароль пользователя MySQL
 define('DB_NAME', 'test'); //Имя базы данных
 define('DB_DATA', 'jqueryvoting'); //Имя таблицы с данными опросов
 define('DB_RESULTS', 'jqueryvotingresults'); //Имя таблицы с результатами опросов
 define('DB_ENCODING', 'cp1251'); //Кодировка базы данных. utf8 для Юникода
 define('DEFAULT_TIMEZONE','Asia/Krasnoyarsk'); //часовой пояс по умолчанию
?>

Вот примерно всё, написать на PHP админку для такого скрипта - абсолютно тривиальная задача, я этого делать не стал. Проект не архивирую, так как в будущем коды могут измениться, все они приведены в этой заметке. А в общем, бред какой-то получился, убил хороший вечер :)

 Архив с этой голосовалкой для PHP 7, MySQLi и Юникода, разверните в новую папку (4 Кб)

10.02.2016, 21:12 [9677 просмотров]


теги: учебное php jquery опрос

К этой статье пока нет комментариев, Ваш будет первым