Списки для голосования используются повсеместно в различных веб проектах. В данном уроке мы создадим простой основанный на PHP список голосования.
Так как для полноценного функционирования демонстрационного примера требуется поддержка сервером SQLite, то в уроке используется демонстрационный пример из оригинального урока. Про подключение SQLlite на сервере можно почитать здесь.
Шаг 1: Планирование и создание базы данных
Для формирования результатов голосования нам надо хранить следующие данные:
Идентификатор вопроса
Идентификатор ответа
Количество голосов, которые получила пара вопрос/ответ
В данном уроке будем использовать PDO и SQLite. Для создания базы данных используется PHP скрипт:
<?php
echo "Создаем базу данных\n";
try {
$dbh = new PDO('sqlite:voting.db');
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$dbh->exec('
CREATE TABLE tally (
QID varchar(32) NOT NULL,
AID integer NOT NULL,
votes integer NOT NULL,
PRIMARY KEY (QID,AID))
');
}
catch(PDOException $e) {
echo "ERROR!!: $e";
exit;
}
echo "База данных успешно создана.";
?>
Структура таблицы
Данный простой скрипт создает базу данных SQLite в директории, где выполняется. В отличие от MySQL, база данных является простым файлом. Запрос SQL CREATE создает таблицу в которой устанавливается основной ключ:
PRIMARY KEY (QID,AID)
Шаг 2: Разрабатываем разметку HTML списка голосования
Прежде, чем начать генерировать код PHP, вам нужно решить, каким образом будет выглядеть список голосования в терминах разметки. Мы собираемся сохранить семантику простой, насколько будет возможно. Список голосования будет иметь два вида:
Вопросы ожидают ответа - вид вопросов
Текущий результат голосования - вид результатов голосования
При написании кода HTML будут использоваться классы CSS, которые будут описаны далее в уроке.
Вид вопросов
Так как список голосования является наборов возможных ответов, то мы собираемся использовать неупорядоченный список, который будет содержать ответы. Для самого вопроса будет использоваться тег заголовка.
<form class="webPoll" method="post" action="test.php">
<h4>Какой вопрос вы хотите задать посетителям?</h4>
<ul>
<li>Ответ</li>
<li>Другой ответ</li>
</ul>
</form>
Очень просто, но нет никаких элементов формы. В данном случае отлично подходят радио кнопки, так как нам нужен только один вариант ответа для учета голоса. Таким образом мы собираемся использовать тег label для установки соответствия ответа с нужной радио кнопкой. Теперь наша форма HTML будет выглядет так:
<form class="webPoll" method="post" action="test.php">
<h4>Какой вопрос вы хотите задать посетителям?</h4>
<ul>
<li>
<label class='poll_active'>
<input type='radio' name='AID' value='0'>
Первый ответ
</label>
</li>
</ul>
</form>
Нужно кое-что добавить. Мы собираемся использовать тег fieldset для задания опций стиля, и конечно же нужна кнопка для отправки голоса!
<form class="webPoll" method="post" action="/poll/test.php">
<h4>Какой вопрос вы хотите задать посетителям?</h4>
<fieldset>
<ul>
<li>
<label class='poll_active'>
<input type='radio' name='AID' value='0'>
Первый ответ
</label>
</li>
</ul>
</fieldset>
<p class="buttons">
<button type="submit" class="vote">Голосовать!</button>
</p>
</form>
Для каждого ответа будет создаваться новый тег li, в котором значение радио кнопки будет увеличиваться. Данная операция будет в конечном итоге выполняться кодом PHP.
Вид результатов голосования
HTML код будет похож на вид вопросов. Теги li не будут содержать элементов формы, а мы добавим div, который будет использоваться для отображения процента голосов, которые получил ответ:
<li>
<div class='result' style='width:20px;'> </div>
<label class='poll_results'>
10%: Первый ответ
</label>
</li>
Здесь используется строчное задание стилей. Данные стили генерируются нашим кодом PHP на основе текущего значения процентов голосов, отданных каждому ответу. Вот так примерно должны выглядеть виды:
Вид ответов
Шаг 3: Стиль формы
Код HTML, который мы только что сделали, выглядит не очень привлекательно. Нужно сделать вид страницы более привлекательным. Мы будем использовать чудесную библиотеку CSS3 PIE (прогрессивный Internet Explorer), чтобы страница выглядела одинаково во всех браузерах.
Стиль для тега form
Мы собираемся использовать тег form как контейнер. Он будет иметь прекрасные скругленные углы и тень. стиль также задает ширину и отступы.
form.webPoll {
background:#ededed;
behavior:url(PIE.php);
border:1px solid #bebebe;
-moz-border-radius:8px;
-webkit-border-radius:8px;
border-radius:8px;
-moz-box-shadow:#666 0 2px 3px;
-webkit-box-shadow:#666 0 2px 3px;
box-shadow:#666 0 2px 3px;
margin:10px 0 10px 8px;
padding:6px;
position:relative;
width:246px;
}
Ключевая строка - та которая содержит атрибут behavior. Она игнорируется всеми браузерами, кроме IE, и добавляет функциональность CSS3 для IE6-8.
Вид панелей
Внешний вид все еще несколько неуклюжий, так что продолжим работу со стилями.
Прямоугольник ответов
Теперь создадим прекрасный прямоугольник ответов. Задаем цвет для внешней границы (fieldset) такой же как и для фона, а неупорядоченный список будем использовать как реальную границу. Вот код CSS:
form.webPoll fieldset {
background:#FCFAFC;
behavior:url(PIE.php);
border:1px solid #FCFAFC;
-moz-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
margin:0;
padding:0;
position:relative;
}
form.webPoll ul {
behavior:url(PIE.php);
border:2px #bebebe solid;
-moz-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
font-family:verdana;
font-size:10px;
list-style-type:none;
margin:0;
padding:10px 0;
position:relative;
}
Модернизированный вид ответов
Стиль для ответов
Теперь нужно добавить стили CSS для того, чтобы наши опции лучше выглядели.
form.webPoll li {
margin:0 16px;
overflow:auto;
padding:4px 0 6px;
position: relative;
}
form.webPoll input {
position: absolute;
top: 4px;
*top: 0;
left: 0;
margin: 0;
padding:0;
}
label.poll_active {
float:right;
width:90%;
}
Почему мы использовали абсолютное позиционирование для ввода для ввода и плавающее позиционирование для меток. Потому что ответ содержит несколько строк. Если ответ в списке голосования будет длинным, то радио кнопка должна выглядеть как метка в неупорядоченном списка - висеть слева от текста. Таким образом мы избегаем обтекания текста если он содержит несколько строк.
Здесь также используется стиль специально предназначенный для IE (с символом *), который выстраивает кнопки в правильном порядке в IE6-8.
Также нужно задать стили для полосок, отображающих результат голосования. Добавим код:
form.webPoll .result {
background: #d81b21;
background: -webkit-gradient(linear, left top, left bottom, from(#ff8080), to(#aa1317));
background: -moz-linear-gradient(top, #ff8080, #aa1317);
-pie-background: linear-gradient(#ff8080, #aa1317);
border:1px red solid;
-moz-border-radius:3px;
-webkit-border-radius:3px;
border-radius:3px;
clear:both;
color:#EFEFEF;
padding-left:2px;
behavior: url('PIE.php');
}
Здесь также есть специфический атрибут -pie-background, который в сочетании с библиотекой PIE дает возможность использовать градиент в фоне в IE.
Вопрос и кнопка
Стиль для тега h4 по умолчанию не соответствует нашим ожиданиям, так что добавим код для него.
form.webPoll h4 {
color:#444;
font-family:Georgia;
font-size:19px;
font-weight:400;
line-height:1.4em;
margin:6px 4px 12px;
padding:0;
}
Также кнопки по умолчанию не очень стильно выглядят, так что будем использовать CSS спрайты для них.
.buttons {
margin:8px 0 1px;
padding:0;
text-align:right;
width:122px;
}
.vote {
background:url(vote.png) repeat scroll 0 0 transparent;
border:medium none;
height:40px;
text-indent:-9999em;
width:122px;
}
.vote:hover {
background-position:0 -41px;
cursor:pointer;
}
Вид панелей
Последние штрихи
Для того чтобы исправить некоторые выверты IE6, добавлем свойство zoom некоторым элементам. Свойство игнорируется всеми остальными браузерами.
form.webPoll ul,li { /*// Делаем IE6 счастливым //*/
zoom:1;
}
Также вы могли заметить границу между вопросами. Она выполняется с помощью дополнительного класса для тега li, который назначается всем элементам списка в PHP скрипте.
Полностью CSS файл доступен в исходниках.
Шаг 4: Создаем класс PHP — выбираем интерфейс
Теперь пришло время создать код PHP для генерации списка голосования, обработки голосов, и вывода результатов. Будем стараться сохранить скрипт простым настолько, насколько возможно. Для создания списка голосования в определенном месте страницы используется следующий код PHP:
$a = new webPoll(array(
'Что будем осваивать дальше?',
'HTML & CSS',
'JavaScript',
'Рабочая среда JS (jQuery, и т.д.)',
'Ruby/Ruby on Rails',
'PHP',
'mySQL'));
Просто передаем конструктору массив, содержащий вопрос за которым следуют ответы. Для того, чтобы отслеживать вопросы в базе данных мы будем использовать хеш MD5 вопроса в качестве идентификатора.
Шаг 5: Выбираем свойства класса
Определенные данные нужны для любого списка голосования. Мы будем хранить некоторые из этих данных в свойствах класса. Нужно сохранять вопросы и ответы, основную разметку HTML, идентификатор вопроса, и информацию о том, как выводить полоску результата. Ниже приводится код PHP:
class webPoll {
# константы инициализации
const POLL = true;
const VOTES = false;
# количество пикселей для 1% на прямоугольнике вывода
public $scale = 2;
# вопрос и ответы
public $question = '';
public $answers = array();
# разметка HTML
private $header = '<form class="webPoll" method="post" action="%src%">
<input type="hidden" name="QID" value="%qid%" />
<h4>%question%</h4>
<fieldset><ul>';
private $center = '';
private $footer = "\n</ul></fieldset>%button%\n</form>\n";
private $button = '<p class="buttons"><button type="submit" class="vote">Голосовать!</button></p>';
# идентификатор вопроса
private $md5 = '';
Константы инициализации используются одним из методов для того, чтобы облегчить определение статуса процесса голосования.
Идентификатор вопроса используется для хранения информации в базе данных. Все значения в коде HTML, заключенные между знаками процента, будут заменяться.
Шаг 6: Создаем код HTML для списка голосования или для ответов
Так как список голосования будет создаваться объектом, разберем метод __construct.
public function __construct($params) {
$this->question = array_shift($params);
$this->answers = $params;
$this->md5 = md5($this->question);
$this->header = str_replace('%src%', $_SERVER['SCRIPT_NAME'], $this->header);
$this->header = str_replace('%qid%', $this->md5, $this->header);
$this->header = str_replace('%question%', $this->question, $this->header);
# отдельный куки для каждого индивидуального набора голосования
isset($_COOKIE[$this->md5]) ? $this->poll(self::VOTES) : $this->poll(self::POLL);
}
В первой строке мы вынимаем вопрос из стека массива с помощью функции array_shift, и сохраняем его в свойстве. Ответы сохраняются в виде массива. Мы также создаем идентификатор вопроса, с помощью функции md5.
Следующие три строчки выполняют замещения значений в коде HTML. Первая устанавливает действие формы для страницы на которой размещен список голосования. Вторая строка помещает идентификатор вопроса в скрытое поле формы. Третья строка помещает наш вопрос в код HTML.
В последней строке в конструкторе мы проверяем, голосовал ли пользователь по данному списку или нет. Если он голосовал, то будут выведены результаты голосования, а если нет - то список голосования.
Шаг 7: Генерируем список голосования
Оба процесса - генерирование списка голосования и генерирование результатов - являются очень похожими операциями. Для того, чтобы придерживаться принципа DRY(Не повторяй себя) разделим процесс создания списка на три метода. Основной метод - poll.
Алгоритм действий
private function poll($show_poll) {
$replace = $show_poll ? $this->button : '';
$this->footer = str_replace('%button%', $replace, $this->footer);
if(!$show_poll) {
$results = webPoll::getData($this->md5);
$votes = array_sum($results);
}
for( $x=0; $x<count($this->answers); $x++ ) {
$this->center .= $show_poll ? $this->pollLine($x) : $this->voteLine($this->answers[$x],$results[$x],$votes);
}
echo $this->header, $this->center, $this->footer;
}
Вот что происходит в данном методе:
Строчки 2 и 3: Кнопка "Голосовать" нам нужно только если пользователь не голосовал. Здесь мы определяем, будем ли мы использовать кнопку или нет. А затем вставляем в код HTML, заменяя %button% либо кодом кнопки, либо пустой строкой.
Строки 6 – 8: если мы не показываем список голосования, значит нам нужны результаты. Здесь мы получаем данные. Также вычисляем общее количество голосов, которое будет использоваться позже для определения процентов.
Строки 11 – 12: Генерируются теги li для нашего кода HTML. В зависимости от того, что мы будем выводить (список голосования или результаты), генерируется различный код HTML. Процесс управляется двумя функциями:
pollLine
voteLine
Строка 15: Простой вывод данных на страницу.
Шаг 8: Метод pollLine()
Это очень простой метод, который получает текущий индекс ответа в качестве аргумента.
private function pollLine($x) {
isset($this->answers[$x+1]) ? $class = 'bordered' : $class = '';
return "
<li class='$class'>
<label class='poll_active'>
<input type='radio' name='AID' value='$x' />
{$this->answers[$x]}
</label>
</li>
";
}
Он проверяет, есть ли еще ответы после текущего, и если есть, то устанавливает CSS класс с границей для данного тега li. Последний ответ не будет иметь данного класса, чем достигается нужный визуальный эффект.
Шаг 9: Метод voteLine()
Данный метод получает три аргумента:
$answer : Ответ на вопрос для данной строчки
$result : Количество голосов, которые получил данный ответ
$votes : Общее количество голосов, участвовавших в опросе
На основании полученных данных генерируется тег li для отображения результатов.
private function voteLine($answer,$result,$votes) {
$result = isset($result) ? $result : 0;
$percent = round(($result/$votes)*100);
$width = $percent * $this->scale;
return "
<li>
<div class='result' style='width:{$width}px;'> </div>{$percent}%
<label class='poll_results'>
$answer
</label>
</li>
";
}
Так как возможно, что ответ не будет иметь голосов вообще. Мы проверяем переменную $result, и если она не имеет значения то ей присваивается по умолчанию 0.
Затем определяется какой процент голосов получил ответ, и с помощью масштаба шкалы устанавливается ширина в пикселах для полоски результата. Затем возвращается код HTML, который соддержит всю необходимую информацию.
Шаг 10: Пишем метод getData()
Метод getData() определен как статический. Зачем? Если мы решим расширить функционал нашей системы в дальнейшем с использованием AJAX, нам нужно будет вызывать данный метод без создания объекта. Вот код метода:
static function getData($question_id) {
try {
$dbh = new PDO('sqlite:voting.db');
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$STH = $dbh->prepare('SELECT AID, votes FROM tally WHERE QID = ?');
$STH->execute(array($question_id));
}
catch(PDOException $e) {
# Ошибка при получении данных, просто отправляем пустой набор данных
return array(0);
}
while($row = $STH->fetch()) {
$results[$row['AID']] = $row['votes'];
}
return $results;
}
ID вопроса передается в метод, который возвращает массив, ID ответов и количество голосов, которые имеет ответ.
Так как ошибка базы данных является катастрофой для системы голосования, то мы просто возвращаем пустой массив, если она происходит. Пользователь получает 0 голосов по каждому ответу. В реальных проектах такие ошибки надо записывать в журнал или отправлять сообщение о них администратору по e-mail.
Шаг 11: Обработка голоса
Мы собираемся добавить второй статический метод к классу. Данный метод будет обрабатывать поступившие голоса. Голос учитывается, если пользователь не голосовал ранее (определяется с помощью куки). Как только пользователь проголосует, мы устанавливаем соответтсвующие куки.
В данном типе веб проиложений очень сложно предотвратить многократное голосование. Установка куки является базовой предосторожностью.
static function vote() {
if(!isset($_POST['QID']) ||
!isset($_POST['AID']) ||
isset($_COOKIE[$_POST['QID']])) {
return;
}
Вызов метода vote() будет размещаться наверху нашей страницы PHP, таким образом нам надо решить, будет обрабатываться голос или нет. Выше приведенное выражение определяет решение:
Если нет идентификатора вопроса в данных POST (ИЛИ!!)
Если нет идентификатора ответа в данных POST (ИЛИ!!)
Куки уже соответствуют идентификатору вопроса
Если любое из данных условий выполняется, то нам не нужно обрабатывать голос, и значит надо покинуть метод.
$dbh = new PDO('sqlite:voting.db');
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
try {
$sth = $dbh->prepare( "INSERT INTO tally (QID,AID,votes) values (:QID, :AID, 1)" );
$sth->execute(array($_POST['QID'],$_POST['AID']));
}
catch(PDOException $e) {
# Ошибка 23000 означает, что ключ уже существует, поэтому надо использовать UPDATE!
if($e->getCode() == 23000) {
try {
$sth = $dbh->prepare( "UPDATE tally SET votes = votes+1 WHERE QID=:QID AND AID=:AID");
$sth->execute(array($_POST['QID'],$_POST['AID']));
}
catch(PDOException $e) {
webPoll::db_error($e->getMessage());
}
}
else {
webPoll::db_error($e->getMessage());
}
}
# Вводим значение $_COOKIE для подтверждения того, что пользователь проголосовал.
if($sth->rowCount() == 1) {
setcookie($_POST['QID'], 1, time()+60*60*24*365);
$_COOKIE[$_POST['QID']] = 1;
}
}
Данный код выглядит более сложным, чем он есть на самом деле. Здесь проверяется, имеет ли определенный ответ хотябы один голос. Если нет, то мы создаем новую запись для ответа и записываем один голос. Если есть, то мы обновляем существующую запись. Как определяется, что нужно сделать?
Используется исключение PDO.
В начале нашего урока мы создали основной ключ по нескольким полям. Когда мы пытаемся вставить запись в таблицу, которая соответствует существующей паре QID/AID , генерируется исключение, которое имеет код 23000 (дублирование основного ключа).
Если операция вставки генерирует исключение, то нам нужно проверить код исключения и, есл ион соответствует 23000, обновить данные в записи. Конечно, если вставка данных произошла по другой причине, или обновление записи завершилось ошибкой, будет вызван метод db_error(), который просто выводит сообщение об ошибке. В случае реального проекта сообщение об ошибке надо записать в журнале и/или сообщить администратору.
Алгоритм процедуры
Завершают метод следующие строки:
# Вводим значение $_COOKIE для подтверждения того, что пользователь проголосовал.
if($sth->rowCount() == 1) {
setcookie($_POST['QID'], 1, time()+60*60*24*365);
$_COOKIE[$_POST['QID']] = 1;
}
}
C помощью rowCount() мы можем проверить, был голос обработан. Если регистрация голоса прошла успешно, то мы устанавливаем куки, используя идентификатор вопроса в качестве имени куки.
В дополнение мы устанавливаем значение для супер-глобальной перменной $_COOKIE, так что при выводе системы голосвания сразу будет показан список результатов.
Шаг 12: Собираем всё вместе
Мы написали PHP, установили CSS и сделали разметку HTML, теперь надо все связать вместе. В данном примере мы просто поместим все на новой пустой странице. На самом верху вставим следующие строки:
<?php
include('webPoll.class.php');
webPoll::vote();
?>
Важно, чтобы данные строки располагались до кода HTML. Потому что если идет процесс голосвания, куки могут быть записаны после того, как что-то будет отправлено. Метод vote() вернет ход процесса обратно, если не пожходящих данных POST для обработки.
Затем мы включаем все созданные стили как отдельную таблицу стилей. Также, надо включить определенный стиль только для IE, который делает доступным псевдо-класс :hover.
<link rel="stylesheet" href="poll.css" type="text/css" />
<!--[if IE]>
<style> body { behavior: url("res/hover.htc"); } </style>
<![endif]-->
В разделе BODY нашей страницы HTMLвставлем следующий код PHP:
$a = new webPoll(array(
'Что будем осваивать дальше?',
'HTML & CSS',
'Javascript',
'Рабочую среду JS (Jquery, и т.д.)',
'Ruby/Ruby on Rails',
'PHP',
'mySQL'));
$b = new webPoll(array(
'Какой вопрос вы хотите задать посетителю?',
'Никакого',
'Почему?',
'Когда?',
'Где?'));
Готово!