Создание опроса/голосования на сайте (PHP)

Форум для тех кто начинает осваивать язык php.
lyod
Сообщения: 76
Поблагодарили: 1 раз

Создание опроса/голосования на сайте (PHP)

Сообщение lyod » Вс мар 08, 2015 11:41 am

Здравствуйте. Предлагаю Вашему вниманию инструкцию по созданию системы голосования на сайте.
Итак, давайте рассмотрим, что же представляет из себя система голосования на сайте.
Пользователь видит на странице форму, в которой присутствуют заголовок голования, варианты ответов, из которых можно выбрать один, тот, за который пользователь хочет проголосовать.
После выбора пользователь нажимает кнопку "проголосовать" и система засчитывает его голос в пользу того или иного варианта ответа.
"Снаружи" вроде всё просто: обычная форма, обычная кнопка, обычные 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.
Но это уже на Ваше усмотрение. :)
Реклама

Вернуться в «PHP»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 4 гостя