Статьи

Создание веб-опроса с помощью PHP

Опросы сегодня почти повсеместно распространены в Интернете, и существует множество сервисов, которые предоставят вам опрос. Но что, если вы хотите написать один самостоятельно? Из этого туториала вы узнаете, как создать простой опрос на основе PHP, включая настройку базы данных, обработку голосования и отображение опроса.


Чтобы сохранить результаты опроса, мы собираемся хранить три фрагмента информации:

  • Идентификатор вопроса
  • Идентификатор ответа
  • Количество голосов, полученных парой вопрос / ответ

Для этого урока мы будем использовать PDO и SQLite. Если вы работаете с SQLite3, вы можете создать новую базу данных с помощью инструмента командной строки; Если вы используете более старую версию, быстрый PHP-скрипт поможет вам. Вот тот, который используется для этого урока:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?php
echo «creating database\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 «db created successfully.»;
?>
Структура базы данных для голосования

Этот простой скрипт создаст базу данных SQLite в каталоге, в котором вы ее запускаете. В отличие от mySQL, база данных здесь представляет собой простой файл. Если вы знакомы с SQL, create должно иметь смысл для вас, хотя для некоторых людей последняя строка может быть новой:

1
PRIMARY KEY (QID,AID)

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


Прежде чем вы начнете писать какой-либо PHP, вам нужно решить, как создать свой опрос с точки зрения разметки. Мы постараемся сделать разметку максимально семантической и простой. Ваш опрос будет иметь два вида:

  • Вопрос ждет ответа
  • Текущие результаты опроса

При написании этого HTML некоторые классы будут включены, чтобы помочь с CSS позже.

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

1
2
3
4
5
6
7
<form class=»webPoll» method=»post» action=»test.php»>
    <h4>What question would you like to ask?</h4>
    <ul>
        <li>Answer Here</li>
        <li>Another Answer Here</li>
    </ul>
</form>

Это довольно просто, но не включает никаких элементов формы. Радио кнопки являются наиболее подходящими, так как мы разрешаем только один ответ на опрос. Также мы собираемся использовать тег label, чтобы связать ответы с нужным переключателем. Теперь наша форма HTML выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
<form class=»webPoll» method=»post» action=»test.php»>
    <h4>What question would you like to ask?</h4>
    <ul>
        <li>
            <label class=’poll_active’>
            <input type=’radio’ name=’AID’ value=’0′>
            First Answer Here
            </label>
        </li>
    </ul>
</form>

Это немного сложнее, но не так уж плохо. Еще немного добавить. Мы собираемся включить тег fieldset, чтобы открыть некоторые параметры стиля, и, конечно, нам нужна кнопка отправки!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<form class=»webPoll» method=»post» action=»/poll/test.php»>
    <h4>What question would you like to ask?</h4>
    <fieldset>
    <ul>
        <li>
            <label class=’poll_active’>
            <input type=’radio’ name=’AID’ value=’0′>
            First Answer Here
            </label>
        </li>
    </ul>
    </fieldset>
    <p class=»buttons»>
        <button type=»submit» class=»vote»>Vote!</button>
    </p>
</form>

Для каждого ответа добавляется новый тег LI и значение переключателя увеличивается. Это в конечном итоге будет сделано нашим PHP. Дополнительный HTML — это тег fieldset и абзац, обернутый вокруг кнопки — оба будут использоваться нашим CSS.

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

1
2
3
4
5
6
<li>
    <div class=’result’ style=’width:20px;’>&nbsp;</div>
    <label class=’poll_results’>
        10%: First Answer Here
    </label>
</li>

Да, это встроенный стиль, который вы видите там. Этот стиль будет сгенерирован нашим PHP на основе текущего процента каждого отдельного ответа. Вот что мы имеем до сих пор:

Unstyled Опрос

HTML-код, который мы создали на последнем этапе, был не очень привлекательным. Давайте посмотрим, сможем ли мы это исправить. Мы собираемся использовать замечательную библиотеку CSS3 PIE (прогрессивный Internet Explorer), чтобы мы могли получить похожий вид во всех браузерах. Чтобы эта библиотека работала должным образом, во многих случаях необходимо применять относительную позицию к элементам. Вы можете прочитать все подробности на сайте библиотеки.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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;
}

Ключевой строкой здесь является атрибут поведения. Это будет игнорироваться браузерами не IE и добавит функциональность CSS3 в IE6-8.

Основной стиль

Все еще некрасиво, но заметное улучшение.

Далее мы собираемся создать красивую рамку вокруг ответов и использовать немного иллюзии, чтобы граница выглядела вставленной в пиксель. Это делается путем окрашивания самой внешней границы (набора полей) того же цвета, что и интерьер, а затем с помощью тега неупорядоченного списка в качестве нашей реальной границы. Вот CSS:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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, чтобы наши параметры выглядели лучше.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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.

Нам также нужно стилизовать панель, используемую для отображения результатов. Мы добавим это сейчас:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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 по умолчанию может не соответствовать тому, что вы ищете, поэтому давайте добавим немного стиля.

1
2
3
4
5
6
7
8
9
form.webPoll h4 {
    color:#444;
    font-family:Georgia, serif;
    font-size:19px;
    font-weight:400;
    line-height:1.4em;
    margin:6px 4px 12px;
    padding:0;
}

И я не большой поклонник кнопок по умолчанию, поэтому мы собираемся использовать CSS-спрайт, чтобы немного оживить его.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
.buttons {
    margin:8px 0 1px;
    padding:0;
    text-align:right;
    width:122px;
}
.vote {
    background:url(res/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? Он не поддерживает псевдо-класс парения! Мы можем либо оставить этих пользователей на морозе (они все равно увидят состояние кнопки по умолчанию), либо мы можем использовать другую симпатичную лицензированную GPL-библиотеку, Whither: hover .

Финальный опрос CSS

Чтобы учесть некоторые особенности IE6, для определенных элементов должно быть запущено что-то под названием «HasLayout». Самый простой способ сделать это — установить свойство zoom для этих элементов. Это свойство игнорируется не браузерами IE.

1
2
3
form.webPoll ul,li { /*// Make IE6 happy //*/
    zoom:1;
}

Вы также заметите, что между каждым вопросом есть границы. Это было сделано с помощью дополнительного класса на тегах LI, определяющих границу. Класс будет назначен всем, кроме последнего элемента, скриптом PHP.

Заполненный файл CSS содержится в загрузке.


Теперь пришло время создать PHP для генерации опросов, отображения результатов и обработки голосов. Я хотел бы продолжать использовать скрипт как можно проще, поэтому я планирую использовать его заранее. Чтобы создать опрос в определенном месте на странице, вы просто используете следующий PHP:

1
2
3
4
5
6
7
8
$a = new webPoll(array(
       ‘What subjects would you like to learn more about?’,
       ‘HTML & CSS’,
       ‘JavaScript’,
       ‘JS Frameworks (jQuery, etc)’,
       ‘Ruby/Ruby on Rails’,
       ‘PHP’,
       ‘mySQL’));

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


Есть определенные данные, которые будут необходимы для каждого опроса; мы собираемся хранить некоторые из них в свойствах класса. Нам нужно будет хранить вопрос и ответы, базовый HTML-код, идентификатор вопроса и некоторую информацию о том, как рисовать панели результатов. Вот начало:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class webPoll {
 
   # makes some things more readable later
   const POLL = true;
   const VOTES = false;
 
   # number of pixels for 1% on display bars
   public $scale = 2;
 
   # the poll itself
   public $question = »;
   public $answers = array();
 
   # the 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»>Vote!</button></p>’;
 
   # question identifier
   private $md5 = »;

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

Обратите внимание на скрытый ввод, который был добавлен здесь. Это идентификатор вопроса, используемый для хранения информации в базе данных. Все значения в HTML, окруженные знаками процента, будут заменены.


Поскольку уже решено, что опрос будет проведен путем создания объекта, давайте рассмотрим метод __construct.

01
02
03
04
05
06
07
08
09
10
11
12
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);
 
    # has the user voted yet?
    isset($_COOKIE[$this->md5]) ?
}

В первой строке мы очищаем вопрос от стека массивов с помощью array_shift и сохраняем его в свойстве. Мы также храним вопросы, оставляя их в виде массива. Здесь мы также создаем идентификатор вопроса, создав хэш md5 для самого вопроса.

Следующие три строки выполняют некоторые замены в HTML. Первый устанавливает действие нашей формы, чтобы указать на страницу, в которой опрос равен единице. Второй помещает наш идентификатор вопроса в скрытое поле формы. Третий ставит наш вопрос в HTML.

В последней строке конструктора мы проверяем, проголосовал ли пользователь в этом конкретном опросе, и, если он проголосовал, мы показываем голоса. Если нет, мы покажем опрос.


И генерация опроса, и генерация результатов очень похожи. Чтобы сохранить наш код сухим, мы разбиваем создание на три метода. Основным является «опрос».

СУХОЙ: не повторяйся
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private function poll($show_poll) {
    $replace = $show_poll ?
    $this->footer = str_replace(‘%button%’, $replace, $this->footer);
 
    # static function doesn’t have access to instance variable
    if(!$show_poll) {
        $results = webPoll::getData($this->md5);
        $votes = array_sum($results);
    }
 
    for( $x=0; $x<count($this->answers); $x++ ) {
        $this->center .= $show_poll ?
    }
 
    echo $this->header, $this->center, $this->footer;
}

Вот разбивка того, что происходит в этой функции:

Строки 2 и 3: нам нужна кнопка голосования, только если пользователь не голосовал. Здесь мы определяем, будем ли мы использовать кнопку HTML или нет, а затем либо вставим HTML, либо заменим заполнитель% button% пустой строкой.

Строки 6 — 8: Если мы не показываем опрос, нам, очевидно, нужны результаты, поэтому мы идем за ними. Мы также рассчитываем общее количество голосов, поданных за использование в дальнейшем при определении процентов.

строки 11 — 12: это генерирует теги LI в нашем HTML. В зависимости от того, показываем ли мы опрос или результаты, мы генерируем другой HTML. Это поколение HTML передается двум функциям:

  • pollLine
  • voteLine

строка 15: просто выводит данные на страницу.


Это очень простой метод, который принимает текущий индекс ответа в качестве аргумента.

01
02
03
04
05
06
07
08
09
10
11
private function pollLine($x) {
    isset($this->answers[$x+1]) ?
    return «
    <li class=’$class’>
            <label class=’poll_active’>
            <input type=’radio’ name=’AID’ value=’$x’ />
                {$this->answers[$x]}
            </label>
    </li>
«;
}

Он проверяет, есть ли ответ после текущего в его первой строке, и, если он есть, применяет класс границ к этому тегу LI. Самый последний ответ не получит этот класс, позволяющий нам достичь намеченного визуального эффекта.


Этот метод получает 3 переданных параметра:

  • $ answer: Ответ на вопрос для этой строки
  • $ результат: количество голосов, которое получила эта опция
  • голосов: общее количество голосов, поданных в этом опросе

С этой информацией могут быть получены теги LI для результатов голосования.

01
02
03
04
05
06
07
08
09
10
11
12
13
private function voteLine($answer,$result,$votes) {
    $result = isset($result) ?
    $percent = round(($result/$votes)*100);
    $width = $percent * $this->scale;
    return «
    <li>
            <div class=’result’ style=’width:{$width}px;’>&nbsp;</div>{$percent}%
            <label class=’poll_results’>
                $answer
            </label>
    </li>
«;
}

Поскольку возможно, что за опцию не будет ни одного голоса, результат $ оставит неустановленным. Если мы обнаружим это, мы дадим ему значение по умолчанию 0 голосов.

Затем мы определяем, какой процент голосов получила опция, и, наконец, используем свойство scale для определения ширины в пикселях, которой должна быть панель результатов. Затем мы наконец возвращаем HTML, содержащий всю эту информацию.


Если вы немного оглянетесь назад, то увидите, что мы вызываем метод getData (), который определен как статический метод в классе. Почему статический? Потому что, если мы решим расширить этот опрос позже, сделав его на основе AJAX, нам понадобится доступ к этому методу без создания объекта. Вот метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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) {
        # Error getting data, just send empty data set
        return array(0);
    }
 
    while($row = $STH->fetch()) {
        $results[$row[‘AID’]] = $row[‘votes’];
    }
 
    return $results;
}

Идентификатор вопроса передается в метод, и он возвращает массив, содержащий идентификаторы ответов и количество голосов, которые имеет ответ. Если ответ не имеет голосов, у него не будет записи в массиве, с которой мы уже работали в методе voiceLine ().

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


Мы собираемся добавить второй статический метод в класс, и он будет обрабатывать входящие голоса. Голоса будут учитываться только в том случае, если пользователь ранее не голосовал (как определено с помощью файла cookie), и после того, как пользователь проголосовал, мы установим файл cookie, указывающий на это.

В этом типе веб-приложений практически невозможно остановить несколько голосов без исключения некоторых законных пользователей. Установка cookie — это лишь основная мера предосторожности.

Это один из более сложных методов в нашем классе webPoll, и мы рассмотрим его в трех частях.

1
2
3
4
5
6
static function vote() {
   if(!isset($_POST[‘QID’]) ||
      !isset($_POST[‘AID’]) ||
      isset($_COOKIE[$_POST[‘QID’]])) {
       return;
   }

Вызов метода голосования () будет в верхней части нашей страницы PHP, поэтому первое, что мы хотим сделать, это решить, есть ли голос для обработки или нет. Вышеупомянутое утверждение — то, как мы определяем это. Вот что это говорит:

  • Если в наших данных POST нет идентификатора вопроса (ИЛИ !!)
  • Если в наших данных POST нет идентификатора ответа (ИЛИ !!)
  • Если файл cookie уже установлен в соответствии с идентификатором вопроса

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
$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 error code means the key already exists, so 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) {
            $this->db_error($e->getMessage());
        }
    }
    else {
        $this->db_error($e->getMessage());
    }
}

Это выглядит намного сложнее, чем на самом деле. Что происходит здесь, мы проверяем, получил ли конкретный ответ голосование раньше. Если этого не произошло, мы создаем новую запись для этого ответа и даем ему один голос. Если это так, мы обновляем существующую запись. Так как же решить, что делать?

ЗОП исключение магия.

Помните, в самом начале мы создали наш многоколонный первичный ключ? Когда мы пытаемся вставить в таблицу запись, которая соответствует существующей паре QID / AID, выдается исключение, в частности код исключения — 23000 (дубликат ключа).

Если вставка выдает исключение, мы собираемся проверить код исключения, а если он соответствует 23000, мы попытаемся обновить запись. Конечно, если вставка не удалась по другой причине или обновление также не удалось, мы просто вызовем метод с именем db_error (), который просто выводит общее сообщение об ошибке. Как и раньше, производственная среда регистрирует эту ошибку и / или уведомляет администратора.

Использование исключений PDO

Наконец, конец метода:

1
2
3
4
5
6
# entry in $_COOKIE to signify the user has voted, if he has
    if($sth->rowCount() == 1) {
        setcookie($_POST[‘QID’], 1, time()+60*60*24*365);
        $_COOKIE[$_POST[‘QID’]] = 1;
    }
}

Используя rowCount (), мы можем убедиться, что мы обновили или вставили голосование. Если голос был успешно зарегистрирован, мы устанавливаем файл cookie, указывающий столько же, используя идентификатор вопроса в качестве имени файла cookie.

В дополнение к настройке файла cookie, мы заполняем суперглобальный $ _COOKIE, поэтому при отображении опроса отображаются ответы, а не повторное представление опроса.


Мы написали PHP, настроили CSS и HTML, теперь пришло время использовать все это. В этом примере мы просто поместим все на страницу, которая в противном случае пуста. В самом верху страницы вставьте следующее:

1
2
3
4
<?php
    include(‘webPoll.class.php’);
    webPoll::vote();
?>

Важно, чтобы это была самая верхняя часть страницы перед любым HTML. Почему? Потому что, если есть голосование для обработки, куки могут быть записаны, и вы не можете писать куки после того, как что-то еще было отправлено. Вызов статического метода голосования () возвращает, если нет необходимых данных POST для обработки.

Далее мы включим все стили, которые мы написали, в отдельную таблицу стилей. Кроме того, мы собираемся включить определенный стиль только для IE, который был упомянут ранее, чтобы включить псевдо-класс: hover.

1
2
3
4
<link rel=»stylesheet» href=»poll.css» type=»text/css» />
<!—[if IE]>
<style> body { behavior: url(«res/hover.htc»);
<![endif]—>

В ТЕЛЕ HTML-страницы вы вставите следующий PHP-код для вставки опросов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
$a = new webPoll(array(
        ‘What subjects would you like to learn more about?’,
        ‘HTML & CSS’,
        ‘JavaScript’,
        ‘JS Frameworks (jQuery, etc)’,
        ‘Ruby/Ruby on Rails’,
        ‘PHP’,
        ‘mySQL’));
 
 
$b = new webPoll(array(
        ‘What is your question?’,
        ‘Don\’t have one’,
        ‘Why?’,
        ‘When?’,
        ‘Where?’));

Это оно! Спасибо за прочтение. Есть мысли, вопросы или предложения?