Web - Системы голосований на РНР - PRCY⮭net
Виват, дорогие читатели ! Сегодня, в этот ничем не знаменательный день, а может быть совершенно наоборот, я написал "это", а сейчас вам предстоит всё "это" прочитать, а самое главное понять. Сегодня мы с вами посвятим время такой теме, как "Системы голосований на РНР".

Да, на сегоднешний день этой довольно интересной теме посвященны целые горы статей, но как-то маловато статей рассказывают всё чётко и во всех мелочах. Именно про эти мелочи и чётко мы сейчас и поговорим. Наверное больше половины из вас участвовали в разных интерактивных голосования, форумных pool'ах, и в прочей дребедени. Но как это всё построено ? На каком алгоритме это всё "пашет" ?

А алгоритмов существует великое множество. Сейчас я перечислю самые популярные среди разработчиков:

XML- вопрос и ответы, храняться в одном XML файле, с которого через парсер и "достают" все данные для обеспечения работы голосований.

Хранение данных в файле - этот тип хранения более популярен чем его XML-ий товарищ. Принцип такой: вопрос помещаеться в первую строку файла, после построчно выводятся варианты ответа, а через некий разделитель количество голосов. Однако в отличие от предыдущего данный метод требует больше времени для написания, и немного ограничивает возможности.

Хранение данных в БД (способ №1) - этот способ являеться самым рациональным, ведь он не требует особых ментальных усилий, при этом обеспечивая высокую продуктивнось, но для него, во-первых, требуеться наличие БД, во-вторых, этот метод занимает много физической памяти базы данных. Мы рассмотрим его в самом начале. Принцип таков: вопрос и прочие статические данных располагаються в некой абстрактной таблице "а", при этом ответы в таблице "б". На каждый ответ приходится ряд "р", таблице "а", который имеет ссылку на номер голосования находящегося в таблице "а". То есть между таблицей "а" и "б" устанавливаеться прямая связь. Почему я сказал, что этот способ требует немалого пространства в БД ? Так потому, что под каждый ответ выделяеться отдельный столбец, а это крайне не рационально в случае при работе с БД.

Хранение данных в БД (способ №2) - этот способ самый сложный среди всех вышеперечисленных, однако он не занимает много место, при этом он совмещает ответы, вопрос, и количество ответов в отной таблице, и в этой таблице ряд отводиться под всё голосование в целом, хотя эта компактность компенсируеться повышеной сложностью обновления данных, и засчёт каждого нового голоса. Принцип в том, что все ответы помещаються в одну строку, при чём в особом порядке, так, что позиции каждого ответа соответствует позиция значения количества голосов, людей которые проголосовали именно за этот вариант. Так же поле вариантов ответов, и соответственно количества голосов будут кодироваться в формате base64, для уменьшения размера конечной строки.

Всё, это были все варианты выполнения алгоритма процесса голосования, которые мы сегодня рассмотрим.

Лично мой выбор второй вариант хранения в БД, так как я автор данного способа, и он мне более по душе, но тем кто только начинает осваивать технологии Веб-программирования, я советую почитать про первый вариант при работе с БД. Что ж, теперь можно перейти к практической части, и она начнёться именно с первого варианта при работе с БД. Но я забыл сказать про довольно важную на мой взгляд деталь. Я забыл сказать про защиту от повторного голосования, это мы будем делать во всех голосования при помощи БД, поскольку именно этот способ обеспечивает полную целостность данных, ибо для доступа к БД третьим лицам им нужно будет иметь данные для доступа или взломать БД, что крайне проблематично, поэтому хакерам будет довольно сложно нанести скрипту урон в этой части. А файлы всегда можно удалить, даже если на них будет блокировка "666", то всё равно они защищены не в полном объёме.

Поэтому я выбрал БД. Посему сейчас вам нужно создать табличку для храния данных о пользователях которые уже проголосовали.

Структура таблички такова:

ТАБЛИЦА `alredy_vote`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
vote_id- BIGINT- NOT NULL- UNIQUED
ip- TEXT- NOT NULL

Название оставляю вам, но скажу что я буду использовать "alredy_voted". Что ж, а теперь на практику :)

Первый вариант при работе с БД.

Что ж, по моему мнению для тех кто называет себя программистами на "достаточном" уровне хватило бы одного только описания в начале, но во всяком случае я опишу данный способ со всеми частицами. Итак, с начала мы создадим две таблички в вашей базе данных, называйте их как хотите, но в статье я буду использовать названия "pools_answs" (для вариантов ответа) и "pools" (для самих голосований). Их структура сдедующая:

ТАБЛИЦА `pools`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
question- TEXT- NOT NULL
status- ENUM('on','off')- DEFAULT 'on'- NOT NULL

ТАБЛИЦА `pools_answs`:
id- BIGINT- AUTO_INCREMENT- PRIMARY KEY
vote_id- BIGINT- NOT NULL- UNIQUED
value- TEXT- NOT NULL

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

Так же хочу упомянуть, что пользователь сам может устанавливать количество вариантов ответа. Конечно есть соблазн использовать DOM модель и любимый нами всеми JS (ведь так ?), но я воздержусь, и воспользуюсь простым и банальным параметром QUERY_STRING и старым, родимым $_GET интерфейсом. Что ж, давайте попытаемся воплотить всё это безобразие на "холсте" :)

<?
$count=isset($_GET['count'])? $_GET['count']:5;
if(!isset($_POST['add'])){
print"<form action='' method='post' name='addPool'>";
print"<table width='300' height='50' align='center'>";
print"<tr><td colspan='2' style='text-align:center;'><input
size='40' type='text' name='question'
value='Введите вопрос голосования' onFocus='this.select();'></td>
</tr>";
print"<tr><td style='text-align:center;' colspan='2'><button
onClick="top.location='".$_SERVER['PHP_SELF']."?count=".($count+1)."'">
Добавить вариант</button></td></tr>";
for($i=0;$i<$count;$i++){
print"<tr><td>Вариант ответа №".$i.":</td><td><input
type='text' name='answs[]'></td></tr>";
}
print"<tr><Td colspan='2' style='text-align:center;'><input
type='submit' name='add' value='Добавить'></td></tr>";
print"</table>";
print"</form>";
}else{
$question=$_POST['question'];
$answs=$_POST['answs'];
if(trim($question)==''){
die("Вы не ввели вопрос !");
}
$count=0;
for($i=(count($answs)-1);$i>=0;$i--){
if(trim($answs[$i])==''){
$count++;
}
if($count==count($answs) || (count($answs)-$count)<2){
die('Должно быть как минимум 2 варианта ответа!');
}
}
$conn_id=@mysql_connect("localhost","root","") or die("Ошибка соединения с
сервером БД !");
@mysql_select_db("shockstudio");
$check=@mysql_query("SELECT id FROM `pools` WHERE question='".$question."'",
$conn_id) or die("Ошибка запроса к БД !");
if(@mysql_num_rows($q)!=0){
die("Голосование с таким вопросом уже существует !");
}
unset($check);
$q=@mysql_query("INSERT into `pools` VALUES('','".$question."','on')",$conn_id)
or die("Ошибка запроса к БД !");
unset($q);
$q=@mysql_query("SELECT id FROM `pools` WHERE question='".$question."'",$conn_id)
or die("Ошибка во время запроса к серверу !");
$row=@mysql_fetch_array($q);
$id=$row['id'];
unset($q,$row);
for($i=(count($answs)-1);$i>=0;$i--){
if(trim($answs[$i])!=''){
$q=@mysql_query("INSERT into `pools_answs` VALUES('','".$id."','".$answs[$i]."',
'')",$conn_id) or die("Ошибка запроса к БД !");
}
}
}
?>

Что ж, это и есть функциональная база для добавления голосования в базу данных. Давайте остановимся на этом и рассмотрим всё детальней.

С самого начала мы получаем количество вариантов ответа, которые нужно вывести для редактирования. Мы объявляем переменную `count`, которая в соответствии от существования переменной `count` в QUERY_STRING будет принимать значение или переменной QUERY_STRING (если такая сущестует) и значение по умолчанию, а именно 5. После этого мы работаем над пользовательским интерфейсом, и при этом создаём интересную кнопочку, которая и служит для добавления варианта ответа. При её нажатии значение переменной `count` помещаеться в строку запроса в броузере, и при этом она увеличиваеться на единицу. Вот как всё просто, прям до ужаса. Далее всё просто, и вот мы перешли к обработке полученных из формы данных.

В начале обработки идёт процесс валидации данных. Мы проверям заполнены ли поля формы, а после этого количество вариантов ответа. Для проверки заполненности полей "ответов", мы объявляем переменную $count, которая будет каждый раз увеличиваться, если $i-й элемент массива будет не заполнен.

После, если количество незаполненных полей ($count), будет равно общей сумме вариантов ответа, или их разница будет менее 2-х, то будет возбужденно исключение, которое сообщит пользователю о том, что нужно ввести минимум 2 варианта ответов. На этом проверка данных завершаеться, и мы переходим к валидации данных в БД. Сначала нам нужно проверить, не существует ли уже голосования с таким вопросом, и если существует то возбуждаем ошибку. Если всё же нет, то добавляем данные в таблицу.

Следующим шагом будет получение индификатора (id) текущей записи, для добавления вариантов ответа в таблицу. После того как мы получили ID, мы снова делаем перебор, только теперь уже с иной целью. Сейчас мы будем добавлять каждый i-й элемент массива, при условии что он не пустой, в таблицу для ответов, при этом мы так же добавляем и индификатор голосования к которому будет привязан данных ответ. Ну и в случае успеха, выводим сообщение о том, что всё прошло успешно. Как видите, без учёта некоторых моментов, всё довольно легко. Но это только первый кусочек той большой мозаики общей функциональностию. Сейчас мы переходим к следующей части, которая и будет неким тестом предыдущей, а именно воплотим сам механизм голосования через интерфейс сайта. В контексте данного варианта исполнения, это займёт почти несколько строк, так как нам всего-то навсего нужно обновить поле в одной таблице, и занести данные в другую.

Что ж, вот мой вариант исполнения данной задачи:

<?
if(!isset($_POST['vote'])){
print"<form action='' method='post' name='vote'>";
print"<table width='400' height='50' align='center'>";
$conn_id=@mysql_connect("localhost","root","") or
die("Ошибка соединения с сервером БД !");
@mysql_select_db("db");
$q=@mysql_query("SELECT * FROM `pools` WHERE status='on'",$conn_id) or
die("Ошибка запроса к БД !");
if(@mysql_num_rows($q)==0){
echo"Голосования не найдены !";
}else{
$id=mt_rand(1,@mysql_num_rows($q));
unset($q);
$q=@mysql_query("SELECT * FROM `pools` WHERE id='".$id."'",$conn_id) or
die("Ошибка запроса к БД !");
$row=@mysql_fetch_array($q);
print"<tr><Td colspan='2'>Q: ".$row['question']."</td>
</tr>";
unset($q);
$vote_check=@mysql_query("SELECT id FROM `alredy_vote` WHERE ip='".
$_SERVER['REMOTE_ADDR']."'",$conn_id) or die("Ошибка запроса к БД !");
$q=@mysql_query("SELECT id,value FROM `pools_answs` WHERE vote_id='".
$id."'",$conn_id) or die("Ошибка запроса к БД !");
if(@mysql_num_rows($q)==0){
die("Вопросы не найдены !");
}else{
while($row=@mysql_fetch_array($q)){
$row2=@mysql_fetch_array($q2);
if(@mysql_num_rows($vote_check)!=0){
$q2=@mysql_query("SELECT count FROM `pools_answs` WHERE id='".$row['id']."'",
$conn_id) or die("Ошибка запроса к БД !");
print"<tr><td>".$row['value']."</td><td>".
$row2['count']."</td></tr>";
}else{
print"<tr><td>".$row['value']."</td><td><input
type='radio' name='answer' value='".$row['id']."'></td></tr>";
print"<input type='hidden' name='id' value='".$id."'>";
print"<tr><td colspan='2'><input type='submit' name='vote'
value='Проголосовать'></td></tr>";
}
}
}
}
print"</table>";
print"</form>";
@mysql_close($conn_id);
}else{
$id=$_POST['id'];
$answer=$_POST['answer'];
$conn_id=@mysql_connect("localhost","root","")
or die("Ошибка во время запроса к серверу !");
@mysql_select_db("db");
$q=@mysql_query("SELECT id FROM `aredy_vote` WHERE ip='".
$_SERVER['REMOTE_ADDR']."'",$conn_id)
or die("Ошибка во время запроса к серверу !");
if(@mysql_num_rows($q)!=0){
print"Вы уже участвовали в данном голосовании !";
}else{
$q=@mysql_query("INSERT into `alredy_vote` VALUES('','".$id."','".
$_SERVER['REMOTE_ADDR']."')",$conn_id) or die("Ошибка запроса к БД !");
unset($q);
$q=@mysql_query("UPDATE `pools_answs` SER count=count+1 WHERE id='".$id.
"' AND vote_id='".$_POST['answer']."'",$conn_id) or die("Ошибка запроса к БД !");
print"Ваш голос учтён. Спасибо за участие !!";
}
@mysql_close($conn_id);
}
?>

Я конечно перегнул на счёт нескольких строк, хотя оно в действительсности так и есть, ведь для обновления данных в БД нужно всего-то навсего 2 запроса к БД. Теперь давайте посмотрим на сомнительные места более пристально. Ну, сначала мы получаем все записи из БД, после делаем рандомизацию

(случайную выборку), ну и после получаем данные по "выпавшему" элементу. Но главным в пользовательском интерфейсе являеться именно то, чтобы обеспечить недоступность для пользователя повторной попытки проголосовать. Для этого мы делаем запрос к таблице `aredy_vote`, где мы проверяем наличие записи у которой ip-адрес равен текущему значению $_SERVER['REMOTE_ADDR'], и если такая действительно найдена, то мы вместо радио-боксов выводим количество голосов, по каждому элементу, и так же убираем кнопочку для отправки данных формы. После вывода пользовательского интерфейса нам необходимо обработать данные поступившие из него. Но что именно нам оттуда понадобиться ?

Нам нужно будет всего-то две переменные, а именно индификатор текущего голосования, и, безусловно, вариант ответа. После этого мы производим запрос к БД, с целью проверки принимал ли данный пользователь участие в голосовании, с той же целью безопастности.

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

И после всех этих манипуляций мы закрываем соединение с БД и благодарим пользователя за участие в нашем голосовании. Вот мы и проделали уже около 40% всей работы над данным проектом, нам ещё осталось три механизма, а какие поговорим дальше. Сейчас я предлагаю вам перейти к следующей ступени разработки проекта, а именно к редактированию уже существующего голосования. Это уже не будет новым для вас. Однако скажу, что при редактировании голосования, мы будем обнулять его результаты.

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

<?
$count=isset($_GET['count'])? $_GET['count'] : 0;
if(!isset($_GET['eid']) || !is_numeric($_GET['eid']){
die("Ошибка при проверке QUERY_STRING!");
}
$_GET['eid']=addslashes($_GET['eid']);
if(!isset($_POST['edit'])){
$conn_id=@mysql_connect("localhost","root","")
or die("Ошибка соединения с сервером БД !");
@mysql_select_db("shockstudio");
$q=@mysql_query("SELECT * FROM `pools` WHERE id='".$_GET['eid']."'",$conn_id)
or die("Ошибка во время запроса к серверу !");
if(@mysql_num_rows($q)==0){
die("Данное голосование не существует!");
}else{
$row=@mysql_fetch_array($q);
print"<form action='' method='post' name='addPool'>";
print"<table width='300' height='50' align='center'>";
print"<tr><td colspan='2' style='text-align:center;'><input
size='40' type='text' name='question' value='".$row['question']."'' onFocus='this.select();'></td></tr>";
unset($q);
print"<input type='hidden' name='id' value='".$_GET['eid']."'>";
$q=@mysql_query("SELECT value FROM `pools_answs` WHERE vote_id='".$_GET['eid'].
"'",$conn_id);
$i=0;
while($row=@mysql_fetch_array($q)){
$i++;
print"<tr><td>Вопрос №".$i.":</td><td><input
type='text' value='".$row['value']."' name='answs[]'></td></tr>";
}
if($count!=0){
for($j=1;$j<$count;$j++){
print"<tr><td>Вопрос №".($j+$i).":</td><td><input
type='text' name='answs[]'></td></tr>";
}
}
}
print"<tr><td colspan='2'><button
onClick="top.location.href='?eid=".$_GET['eid']."&count=".($count+1)."';">
Добавить вариант ответа</button></td></tr>";
print"<tr><Td colspan='2' style='text-align:center;'><input
type='submit' name='edit' value='Изменить'></td><tr>";
print"</table>";
print"</form>";
}else{
$question=$_POST['question'];
$id=$_POST['id'];
$answs=$_POST['answs'];
if(trim($question)==''){
die("Вы не ввели вопрос !");
}
$count=0;
for($j=(count($answs)-1);$j>=0;$j--){
if(trim($answs[$j])==''){
$count++;
}
if($count==count($answs) || (count($answs)-$count)<2){
die('Должно быть как минимум 2 варианта ответа');
}
}
$conn_id=@mysql_connect("localhost","root","")
or die("Ошибка соединения с сервером !");
@mysql_select_db("shockstudio");
$q=mysql_query("UPDATE `pools` SET question='".$question."' WHERE id='".$id.
"'",$conn_id) or die("Ошибка запроса к БД !");
unset($q);
$q=@mysql_query("DELETE FROM `pools_answs` WHERE vote_id='".$id."'")
or die("Ошибка запроса к БД !");
for($i=(count($answs)-1);$i>=0;$i--){
$q=@mysql_query("INSERT into `pools_answs` VALUES('','".$id."','".$answs[$i].
"','')") or die("Ошибка запроса к БД !");
}
print"<hr/><a href='".$_SERVER['PHP_SELF']."?eid=".$id.
"'>Назад</a><hr/>";
}
?>

Что ж, в этом приложении совместились куски всего того, что мы делали в начале статьи. Пользовательский интерфейс данного приложения очень напоминает интерфейс для добавления голосования, однако здесь есть некоторые отличия. Сначала мы получаем данные по переданному индификатору.

Так как для редактирования элемента в скрипт нужно передать его ID в базе данных, по которому будут обрабатываться данные. Но в первую очередь стоит проверить существует ли данное голосование вовсе, так как мало ли что пользователь может вписать в QUERY_STRING, и кстати именно по этой причине мы производим её проверку и экранируем все данные в ней, для предотвращения SQL-инъекций. После всех проверок, мы получаем данные по голосования с индификатором $_GET['eid']; Теперь мы подставляем их в текстовые поля формы. Но всё бы и ничего, но ведь в случая с ответами, нам нужно не только подставить значения для редактирования, но и иметь возможность добавлять новые элементы. Потому, мы с начала выводим все элементы, имеющиеся в таблице с ответами, а вне цикла while() мы создаём ещё один цикл, который будет выводить текстовые поля в соостветствии со значением переменной $count. Замечу, что в отличии от скрипта добавления, данная переменная будет иметь значение по умолчанию, равное 0, а не пяти. Всё, с пользовательским интерфейсом разобрались, теперь плавно переходим к процесу обработки данных формы. С начала всё банально, проверяем количество заполненных полей "ответов", и конечно не оставил ли пользователь пустым поле для текстового значения вопроса. Если всё нормально, то соединяемся с БД.

Первый наш запрос будет по обновлению значения вопроса голосования, это ничего нового для вас не откроет.

После этого, как я говорил раньше, мы удаляем все текущие элементы из таблицы ответов, и снова создаём аналогичный первому цикл, только в этот раз, мы будет добавлять данные в таблицу. Вот видите, я же говорил, что аналогия между скриптом для добавления голосований прослеживаеться ?

Ну, и в случае успеха, мы ставим ссылочку обратно, то есть на некую "главную" страницу, и передаём тот же индификатор голосования, для проверки всё ли прошло успешно и были ли данные занесены в таблицу. Ну, и теперь нам осталось лишь несколько шагов которые и приведут нас к желаемому результату, а именно механизмы блокировки голосования и его восстановления. Что я имею ввиду под "блокировкой" ?

Ну, предположим голосование в активе уже более месяца, и вам хочеться его скрыть, не удаляя, а вдруг опять захочеться его опубликовать ?

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

Это будет производиться как нельзя просто, нам просто нужно будет изменить значение индификатора status, в таблице `pools`. Ведь вы заметили, что при рандомизации и выборке для вывода в пользовательский интерфейс сайта, мы проверяли значение поля status на равенство значению 'on'.

Принципом же блокировки и публикации будет банальное изменение значения status, посему, если вы считаете что это легко, то я советую вам не читать дальше, поскольку действительно, многие из вас нового здесь ничего не отыщут. Вот механизм блокировки, хочу учесть, что скрипт работает под контекстом того, что ему был передан параметр $_GET['eid'], который являеться индификатором текущего голосования.

<?
if(!isset($_GET['eid']) || !is_numeric($_GET['eid'])){
die("Ошибка проверки QUERY_STRING !");
}
$_GET['eid']=addslashes($_GET['eid']);
$conn_id=@mysql_connect("locahost","root","")
or die("Ошибка соединения с сервером !");
@mysql_select_db('db");
$q=@mysql_query("SELECT * FROM `pools` WHERE id='".$_GET['eid']."'",$conn_id)
or die("Ошибка во время запроса к серверу !");
if(@mysql_num_rows($q)==0){
die("Данное голосование не существует !");
}else{
$q=@mysql_query("UPDATE `pools` SET status='off' WHERE id='".$_GET['eid'].""",
$conn_id) or die("Ошибка во время запроса к серверу !");
}
@mysql_close($conn_id);
?>

И для публикации.
<?
if(!isset($_GET['eid']) || !is_numeric($_GET['eid'])){
die("Ошибка проверки QUERY_STRING !");
}
$conn_id=@mysql_connect("locahost","root","")
or die("Ошибка соединения с сервером !");
@mysql_select_db('db");
$q=@mysql_query("SELECT * FROM `pools` WHERE id='".$_GET['eid']."'",$conn_id)
or die("Ошибка во время запроса к серверу !");
if(@mysql_num_rows($q)==0){
die("Данное голосование не существует !");
}else{
$q=@mysql_query("UPDATE `pools` SET status='on' WHERE id='".$_GET['eid'].""",
$conn_id) or die("Ошибка во время запроса к серверу !");
}
@mysql_close($conn_id);
?>

Всё, на этом я заканчиваю рассмотрение данного механизма голосований, как и этот выпуск данной статьи. Со всеми остальными технологиями мы познакомимся в следующих выпусках, так как для одной статьи это слишком большой объём информации. На этом я с вами прощаюсь.
Information
  • Posted on 27.04.2013 15:43
  • Просмотры: 1080