Здравствуйте. Предлагаю Вашему вниманию инструкцию по созданию системы голосования на сайте.
Итак, давайте рассмотрим, что же представляет из себя система голосования на сайте.
Пользователь видит на странице форму, в которой присутствуют заголовок голосования, варианты ответов, из которых можно выбрать один, тот, за который пользователь хочет проголосовать.
После выбора пользователь нажимает кнопку "проголосовать" и система засчитывает его голос в пользу того или иного варианта ответа.
"Снаружи" вроде всё просто: обычная форма, обычная кнопка, обычные radio-кнопки. Но давайте заглянем "за кулисы" работы скрипта голосования. Там нашему взору откроется более интересная картина.
Что же представляет из себя система голосования изнутри?
Как можно запоминать выбор пользователя и не давать ему голосовать повторно?
Как создавать вопросы и варианты ответов?
На эти вопросы мы сейчас попробуем получить развёрнутый ответ. Нам нужно где-то хранить вопросы и варианты ответов. Для этого нам потребуется создать базу данных MySQL и пару таблиц в ней.
Давайте создадим такие таблицы:
voting - таблица, в которой будут храниться вопросы и варианты ответов.
voted - таблица, в которой будут храниться выбранные варианты ответов и IP адреса проголосовавших.
В таблице voting нам нужно создать три поля:
1. id - тип INTEGER, AUTOINCREMENT.
В поле будет содержаться идентификатор вопросов и ответов.
2. parent_id - тип INTEGER.
В поле будет содержаться идентификатор родительской записи (т.е. по отношению к ответам, родительской записью будет вопрос).
3. title - тип VARCHAR.
В поле будет содержаться текст вопросов и ответов.
Как видите, мы будем хранить вопросы и ответы в одной таблице. Поскольку нам в данном случае нужна связь один-ко-многим и поля, необходимые для хранения вопросов и ответов одинаковые,- это очень удобный вариант.
Тут плюс в том, что не нужно создавать отдельные таблицы для вопросов и ответов, и запросов в БД нужно будет делать меньше.
В таблице voted нам нужно создать два поля:
1. answer_id - тип INTEGER.
В поле будет содержаться идентификатор выбранного пользователем ответа (т.е. ответа, за который он проголосовал).
2. ip - тип VARCHAR. UNIQUE.
В поле будет содержаться IP-адрес пользователя, который проголосовал. В данном поле будут только уникальные значения IP.
На этом моменте остановимся поподробнее. Почему мы запоминаем IP пользователя? Ведь IP бывают динамическими, скажете Вы. И пользователь, сменив IP, сможет проголосовать несколько раз. Я с Вами полностью согласен, но ещё не придумано 100% надёжного варианта запонимания неавторизованного пользователя.
Почему я упомянул авторизацию: если мы хотим, чтобы пользователь гарантированно не смог проголосовать несколько раз, нам нужно сделать систему авторизации и заместо IP пользователя использовать для учёта его голоса идентификатор его аккаунта.
И сделать открытым голосование только для авторизованных пользователей. Но создание голосования для авторизованных не входит в планы данной статьи, поэтому для учёта голоса будем запоминать IP.
Итак, мы создали таблицы, теперь давайте перейдём непосредственно к реализации php-скрипта системы голосования. Давайте разберём вот такой класс:
<?php
class Voting{
/**
*
* @var Ресурс соединения с БД
*
*/
private $db;
/**
*
* @var Таблица с вопросами
*
*/
private $tbl_voting = 'voting';
/**
*
* @var Таблица с голосами
*
*/
private $tbl_voted = 'voted';
/**
*
* @var ID голосования
*
*/
private $id;
/**
*
* @var Массив с данными о голосовании
* Заполняется в методе get()
*
*/
private $result = array();
/**
* Для запуска необходимых данных
* @param integer $id - идентификатор голосования
*
* @return
*/
public function __construct( $id=0 ){
# Настройки подключения к БД
$dsn = "mysql:host=localhost;dbname=voting;charset=utf8;";
# Соединяемся с БД
$this->db = new PDO($dsn, 'root', '');
# Устанавливаем ID голосования
$this->id = $id;
}
/**
* Для выборки из базы голосования по установленному ID
*
* @return заполняет массив $this->result данными
*/
private function selectVoting(){
# SQL-запрос для выборки опроса
$stmt = $this->db->prepare(
"SELECT id, title, 0 as voted
FROM `$this->tbl_voting`
WHERE id=:id AND parent_id=0
UNION
SELECT o.id, o.title, COUNT(v.ip)
FROM `$this->tbl_voting` o
LEFT JOIN `$this->tbl_voted` v
ON v.answer_id = o.id
WHERE parent_id=:id
GROUP BY o.id" );
# Выполняем запрос
$stmt->execute(array(
':id' => $this->id
));
# Получаем ассоциативный массив
$this->result['voting'] = $stmt->fetchAll( PDO::FETCH_ASSOC );
# Если данных нет
if( !$this->result )
# Бросаем исключение
throw new Exception('Голосования с ID '. $this->id .' нет');
}
/**
* Для проверки, голосовал ли уже пользователь
*
* @return
*/
private function checkAlreadyVoted(){
# SQL-запрос для выборки опроса
$stmt = $this->db->prepare(
"SELECT COUNT(*)
FROM `$this->tbl_voted`
WHERE ip=:ip AND answer_id IN(
SELECT id FROM `$this->tbl_voting` WHERE parent_id=$this->id
)" );
# Выполняем запрос
$stmt->execute(array(
':ip' => $this->getIP()
));
# Записываем полученные данные в конечный массив
$this->result['already_voted'] = (bool) $stmt->fetch( PDO::FETCH_COLUMN );
}
/**
* Для разбора массива, установленного в select()
*
* @return заполняет массив готовыми для шаблона данными
*/
private function prepare(){
# Получаем из массива первый элемент (вопрос)
$ask = array_shift( $this->result['voting'] );
# Записываем готовые данные в итоговый массив
$this->result = array(
'title' => $ask['title'],
'options' => $this->result['voting'],
'already_voted' => $this->result['already_voted']
);
}
/**
* Для получения IP-адреса
*
* @return IP-адрес пользователя
*/
private function getIP(){
return
getenv('REMOTE_ADDR');
}
/**
* Для получения массива с информацией о голосовании
* @param integer $id - идентификатор голосования,
* которое нужно вывести
*
* @return
*/
public function get(){
# Вызываем метод выборки данных из БД
$this->selectVoting();
# Вызываем метод проверки, голосовал ли пользователь
$this->checkAlreadyVoted();
# Вызываем метод обработки полученных данных
$this->prepare();
# Возвращаем массив данных
return
$this->result;
}
/**
* Для добавления голоса к опросу
*
* @return
*/
public function add( $id ){
# Подготавливаем запрос для добавления голоса
$stmt = $this->db->prepare(
"INSERT IGNORE INTO `$this->tbl_voted`
VALUES ((SELECT id FROM `$this->tbl_voting` WHERE id=:id), :ip)" );
# Выполняем запрос
$stmt->execute(array(
':id' => $id,
':ip' => $this->getIP()
));
}
}
Класс довольно неплохо прокомментирован, поэтому разберём только самые интересные методы данного класса. Для начала нам нужно настроить соединение с нашей базой банных. Для этого в конструкторе нужно указать хост, имя базы данных, имя пользователя и пароль.
Как видите, в мы используем для работы с базой данных расширение PDO. С этим расширением очень удобно работать, и при правильном составлении запросов ещё и безопасно, никакие SQL-инъекции не будут страшны.
Если у Вас возникли затруднения в настройке соединения с базой данных, обратитесь к документации PDO: http://php.net/manual/ru/pdo.connections.php
Теперь давайте разберём метод выборки вопроса и соответствующих ему ответов:
private function selectVoting(){
# SQL-запрос для выборки опроса
$stmt = $this->db->prepare(
"SELECT id, title, 0 as voted
FROM `$this->tbl_voting`
WHERE id=:id AND parent_id=0
UNION
SELECT o.id, o.title, COUNT(v.ip)
FROM `$this->tbl_voting` o
LEFT JOIN `$this->tbl_voted` v
ON v.answer_id = o.id
WHERE parent_id=:id
GROUP BY o.id" );
# Выполняем запрос
$stmt->execute(array(
':id' => $this->id
));
# Получаем ассоциативный массив
$this->result['voting'] = $stmt->fetchAll( PDO::FETCH_ASSOC );
# Если данных нет
if( !$this->result )
# Бросаем исключение
throw new Exception('Голосования с ID '. $this->id .' нет');
}
Как видите, здесь мы используем prepared statement (подготовленные запросы) PDO. В самом запросе мы указываем якорь ":id", а затем в методе "execute" указываем, каким значением заменить данный якорь.
В данном SQL запросе мы используем объединение результатов запросов помощью UNION. Первым SELEСЕ'ом мы выбираем необходмый вопрос (идентификатор которого содержится в свойстве "id" данного класса), а
вторым выбираем принадлежащие данному вопросу варианты ответов и количество голосов за определённыый вопрос (для вывода статистики).
В результате данного запроса мы получаем массив, первым элементом которого является вопрос, а остальными элементами - варианты ответа на вопрос. Теперь нам нужно отделить вопрос от ответов. Для этого мы используем метод "prepare":
/**
* Для разбора массива, установленного в select()
*
* @return заполняет массив готовыми для шаблона данными
*/
private function prepare(){
# Получаем из массива первый элемент (вопрос)
$ask = array_shift( $this->result['voting'] );
# Записываем готовые данные в итоговый массив
$this->result = array(
'title' => $ask['title'],
'options' => $this->result['voting'],
'already_voted' => $this->result['already_voted']
);
}
C помощью array_shift мы вырезаем вопрос из массива и вставляем текст вопроса в результирующий массив (который передадим уже на вывод). Как видите, здесь встречается такой элемент, как "already_voted".
В нём мы содержим информацию, голосовал ли пользователь ранее, т.е. содержится ли IP пользователя в таблице "voted". Проверка на наличие IP в таблице производится в методе "checkAlreadyVoted".
Ну что, с данным классом мы разобрались, теперь сохраните его в файл (назовите его "voting.class.php") и скопируйте к себе на сервер. Далее давайте разберём, как мы будем выводить голосование на экран.
Сначала нам нужно подключить вышеописанный класс на страницу, где будем выводить голосование. Я не знаю, каким образом работает Ваша CMS, поэтому давайте подключим, используя обычный "require":
require('путь к файлу/voting.class.php');
Отлично, класс подключён. Можно с ним работать. Но у нас пока нет ни одного голосования,- давайте создадим его. Для этого нужно зайти в phpMyAdmin, выбрать таблицу "voting" и вставить в неё вопросы и ответы.
Допустим, мы хотим узнать от пользователей, как они относятся к нашему сайту. Для этого в поле "title" вставим такой вопрос: "Как Вам наш сайт?". В поле "parent_id" нужно вставить "0", так как это вопрос и он не является дочерней записью другого вопроса.
После сохранения мы видим, что у нас появилась новая запись в таблице. Теперь мы знаем её идентификатор (находится в поле "id"), это и есть идентификатор вопроса, по этому идентификатору мы привяжем к вопросу варианты ответов на него.
Теперь создадим варианты ответов, для этого в поле "title", например впишем "Отлично". Эта запись и будет одним из вариантов ответа. В поле
parent_id
нам нужно указать идентификатор вопроса, к которому принадлежит данный ответ.
Таким образом создайте необходимое количество вариантов ответов, не забывая в поле
parent_id
указывать идентификатор вопроса.Что ж, мы успешно создали вопрос и варианты ответов на него в базе данных. Теперь перейдём к выводу голосования на страницу.
Для того, чтобы вывести необходимое голосование, нам нужно создать экземпляр объекта класса, который мы подключили на страницу ранее:
try{
# Получаем голосование. В качестве аргумента в конструктор передаём идентификатор вопроса.
$voting = new Voting(1);
# Получаем голосование
$data = $voting->get();
}
catch(Exception $e){
echo $e->getMessage();
}
В коде выше мы видим, что у нас создаётся объект класса, в качестве аргумента конструктору которого передаётся ID вопроса. Как видите, нам не нужно перечислять ID всех записей в БД, принадлежащих данному голосованию. Достаточно указать лишь ID вопроса, а дочерние записи (т.е. у которых в поле "parent_id" содержится ID данного вопроса)
подтянутся вместе с ним, с помощью UNUON, о котором мы говорили ранее. Сейчас мы передаём ID "1", если у Вашего вопроса идентификатор другой - передайте его, единица здесь только для примера.
Если Вы всё правильно сделали, в итоге, в переменной
$data
у Вас будет примерно такой массив:
array(3) {
["title"]=>
string(30) "Как Вам наш сайт?"
["options"]=>
array(3) {
[0]=>
array(3) {
["id"]=>
string(1) "2"
["title"]=>
string(27) "Первый вариант ответа"
["voted"]=>
string(1) "0"
}
[1]=>
array(3) {
["id"]=>
string(1) "3"
["title"]=>
string(13) "Второй вариант ответа"
["voted"]=>
string(1) "0"
}
[2]=>
array(3) {
["id"]=>
string(1) "4"
["title"]=>
string(27) "Третий вариант ответа"
["voted"]=>
string(1) "0"
}
}
["already_voted"]=>
bool(false)
}
Теперь нам нужно создать форму, в которой это голосование будет отображаться. Напишем такой код (код просто для примера, Вы можете стилизовать его под свой сайт, добавив необходимые теги):
<?php if( $data['already_voted'] ): ?>
<? foreach( $data['options'] as $option ):?>
<?=$option['title']?> - <?=$option['voted']?> голосов<br>
<?php endforeach;?>
<?php else:?>
<form method="POST" action="/vote.php">
<? foreach( $data['options'] as $option ):?>
<label>
<input type="radio" name="answer" value="<?=$option['id']?>"/> <?=$option['title']?>
</label><br>
<?php endforeach;?>
<input type="submit" name="submit" value="Голосовать"/>
</form>
<?php endif; ?>
Как мы видим, в коде проводится проверка, голосовали ли пользователь ранее:
<?php if( $data['already_voted'] ): ?>
Если пользователь уже голосовал (т.е. его IP есть в таблице voted), то ему выводится на экран статистика голосов, т.е. ответы и количество пользователей, проголосовавших за тот или иной ответ. Тут Вы можете подключить фантазию и доработать скрипт, например, сделав вывод статистики в виде графика.
Если же пользователь не принимал участие в данном голосовании, то ему выводится форма, в которой он может выбрать необходимый вариант ответа и проголосовать за него.
В цикле мы проходим по всем вариантам ответов и выводим их на экран. В качестве значения в поля "radio" вставляются их идентификаторы:
<input type="radio" name="answer" value="<?=$option['id']?>"/>
Допустим, пользователь выбрал необходимый ответ и нажал "Голосовать". После этого он перейдёт к скрипту "voted.php", как мы видим в коде, "action" формы ведёт именно на этот скрипт:
<form method="POST" action="/vote.php">
Теперь создадим файл "vote.php" и напишем в нём такой код:
<?php
# Подключаем класс голосования
require('voting.class.php');
try{
# Если нажата кнопка "Голосовать"
if( isset( $_POST['submit'] ) ){
# Получаем ID ответа, за который голосуют
$id = filter_input( INPUT_POST, 'answer', FILTER_SANITIZE_NUMBER_INT );
# Если ID не указан
if( !$id )
# Бросаем исключение
throw new Exception('Error!');
# Создаём экзепляр класса работы с голосованием
$voting = new Voting;
# Засчитываем голос
$voting->add( $id );
# Перенаправляем назад
header( 'Location: ' . getenv('HTTP_REFERER') );
}
}
catch(Exception $e){
# Выводим сообщение
echo $e->getMessage();
}
Как видите, в нём мы тоже подключаем класс голосования. Затем проводится проверка, нажата ли кнопка "Голосовать". Если нажата - получаем ID ответа, за который голосует пользователь.
Так же создаётся экземпляр класса голосования, только теперь мы не передаём ID вопроса в конструктор, тут он не требуется, так как опрерируем на данном этапе только с ответом.
Затем мы вызываем метод "add", передавая в него идентификатор вопроса:
$voting->add( $id );
В методе "add" мы используем INSERT IGNORE для того, чтобы IP пользователя повторно не записывался в таблицу, ведь лишние записи нам ни к чему.
После того, как голос пользователя учтён или проигнорирован (в случае, если IP уже есть в таблице), идёт перенаправление его на предыдущую страницу:
header( 'Location: ' . getenv('HTTP_REFERER') );
Ну вот и всё, голосование готово. Её можно ещё конечно доработать, добавив возможность создавать и удалять голосования из админки, добавив внешние ключи для таблиц, чтобы при удалении вопроса из базы удалялись и все связанные с ним вопросы и записи в таблице
voted
.
Но это уже на Ваше усмотрение.