Advertisement

Creating a Web Poll with PHP

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

Polls are nearly ubiquitous on the web today, and there are plenty of services that will provide a drop-in poll for you. But what if you want to write one yourself? This tutorial will take you through the steps to create a simple PHP-based poll, including database setup, vote processing, and displaying the poll.


Step 1: Plan & Create the Database

In order to store poll results, we're going to store three pieces of information:

  • A question identifier
  • An answer identifier
  • The number of votes a question/answer pair has gotten

For this tutorial, we'll be using PDO and SQLite. If you're working with SQLite3, you can create a new database via the command line tool; if you're using an older version, a quick PHP script will do the trick. Here's the one used for this tutorial:

<?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.";
?>
Voting Database Structure

This simple script will create a SQLite database in the directory you run it. Unlike mySQL, the database here is a flat file. If you're familiar with SQL, the create should make sense to you, although the last line may be new to some people:

PRIMARY KEY (QID,AID)

This creates a composite key for the database. Entries in either column do not have to be unique to that column, but the combination of the two must be unique.


Step 2: Design Your Poll's HTML

Before you start writing any PHP, you need to decide how to create your poll in terms of markup. We're going to try to keep the markup as semantic and simple as possible. Your poll will have two looks:

  • Question waiting to be answered
  • Current Results of the poll

In writing this HTML, some classes will be included to help with the CSS later.

Poll View

Because a poll is primarily a list of answers, we're going to incorporate an unordered list to contain those answers. For the question itself, we're going to use a heading tag.

<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>

That's pretty basic, but doesn't include any form elements. Radio buttons are the most appropriate since we're only allowing one answer per poll. Also, we're going to use the label tag to associate answers with the proper radio button. Now our form HTML looks more like this:

<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>

That's a little more complex, but not too bad. Still a bit more to add. We're going to include a fieldset tag to open up some styling options, and of course we need a submit button!

<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>

For each answer, a new LI tag is added and the value of the radio button incremented. That will eventually be done by our PHP. The extra HTML is the fieldset tag, and the paragraph wrapped around the button - both of which will be used by our CSS.

Answer View

The HTML is going to be nearly identical for the answer view. The line-item tags won't contain a form element and we'll be adding a div which can be used to show the percentage of votes that answer received. Here's how that will look:

<li>
    <div class='result' style='width:20px;'>&nbsp;</div>
    <label class='poll_results'>
        10%: First Answer Here
    </label>
</li>

Yes, that's an inline style you see there. That style will be generated by our PHP based on the current percentage of each individual answer. Here's what we have so far:

Unstyled Poll

Step 3: Style the Form

The HTML we created in the last step was not very attractive. Let's see if we can fix that up a bit. We're going to use the wonderful CSS3 PIE (progressive Internet Explorer) library so we can obtain a similar look across all browsers. To make this library work properly, there are numerous cases where you must apply a relative position to elements. You can read all the details on the library website.

Style the Form Tag

We're going to use the form tag as our container. It's going to have nice, rounded corners, and a bit of a drop shadow. The styles below also specify a width, and padding.

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;
}

The key line here is the behavior attribute. This will be ignored by non-IE browsers and will add the CSS3 functionality to IE6-8.

Basic styling

Still ugly, but a noticeable improvement.

Box the Answers

Next, we're going to create a nice box around the answers and use a bit of illusion to make the border look inset by a pixel. This is done by coloring the outer-most border (the fieldset) the same color as the interior, and then using the unordered-list tag as our real border. Here's the 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;
}
Basic styling

Style the Answers

Next we need to add a little CSS to make our options look better.

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%;
}

You might ask why we're using absolute positioning on the inputs and floating the label. The reason is simple: multi-line answers. If an answer to your poll question is long, you want the radio button to look like a bullet on an unordered list - hanging. This will keep the text from wrapping around it if it's multiple lines.

There's also a style targeting IE specifically with the * hack to cause the buttons to line up properly in IE6-8.

We also need to style the bar used to show results. We'll add that now:

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');
}

There's another new attribute here: -pie-background, which allows us to, in conjunction with the PIE library, use gradient backgrounds in IE. There's still a few touches to add.

Question and Button

A default H4 may not be what you're looking for, so let's add some styling to that.

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;
}

And I'm not a big fan of default buttons, so we're going to use a CSS sprite to liven it up a bit.

.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;
}

What about IE6? It doesn't support the hover psudo-class! We can either leave those users out in the cold (they'll still see the button default state) or we can use another lovely little GPL licensed library, Whatever:hover.

Final Poll CSS

Last Bits

In order to accommodate some IE6 quirks, certain elements need to have something called "HasLayout" triggered. The easiest way to do this is to set a property of zoom for these elements. The property is ignored by non-IE browsers.

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

You'll also notice there are borders between each question. This was done with an additional class on the LI tags specifying a border. The class will be assigned to all but the last item by the PHP script.

The completed CSS file is contained in the download.


Step 4: Create a PHP Class — Decide on the Interface

Now it's time to create the PHP to generate polls, show results, and handle votes. I'd like to keep using the script as simple as possible, so I'm planning the usage ahead of time. To create a poll in a particular place in a page, you'll just use the following PHP:

$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'));

That's it. You'll pass an array to the constructor which contains the question followed by the answers. In order to track the questions in the database, we'll create an MD5 hash of the question to use as an identifier.


Step 5: Decide on the Class Properties

There's certain data that's going to be needed by each poll; we're going to store some of this in class properties. We'll need to store the question and the answers, the basic HTML, the question identifier, and some information on how to draw the results bars. Here's the start:

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 = '';

The initial constants will be used in one of the methods to make it more readable so it's easier to know whats going on.

Take note of the hidden input that's been added here. This is the question identifier used to store information in the database. All the values in the HTML surrounded by percent signs will be replaced.


Step 6: Create the HTML Poll or Answers

Since it's already been decided the poll will be made by creating an object, let's examine the __construct method.

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]) ? $this->poll(self::VOTES) : $this->poll(self::POLL);    
}

In the first line, we peel the question off the array stack with array_shift, and store it in a property. We also store the questions, leaving them as an array. We also create the question identifier here, by making an md5 hash of the question itself.

The next three lines perform some replacements on the HTML. The first sets our form action to point at the page the poll is one. The second puts our question identifier in a hidden form field. The third puts our question into the HTML.

In the final line of the constructor we check if the user has voted on this particular poll, and if he has, we show the votes. If he hasn't, we show the poll.


Step 7: Generate the Poll

Both generating the poll and generating the results are very similar operations. In order to keep our code DRY we break the creation into three methods. The main one is "poll".

DRY: Don't Repeat Yourself
private function poll($show_poll) {
    $replace = $show_poll ? $this->button : '';
    $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 ? $this->pollLine($x) : $this->voteLine($this->answers[$x],$results[$x],$votes);
    }

    echo $this->header, $this->center, $this->footer;
}

Here's the breakdown of what's going on in this function:

lines 2 & 3: We only need a vote button if the user hasn't voted. Here we determine if we're going to use the button HTML or not, and then either insert the HTML, or replace the %button% placeholder with an empty string.

lines 6 - 8: If we're not showing the poll, we obviously need the results, so here we go fetch them. We also calculate the total votes cast for use later in determining percentages.

lines 11 - 12: This generates the LI tags in our HTML. Depending on if we're showing the poll or the results, we generate different HTML. This HTML generation is handed off to two functions:

  • pollLine
  • voteLine

line 15: Simply dumps out the data to the page.


Step 8: The pollLine() Method

This is a very simple method, which takes the current index of the answer as an argument.

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>
";
}

It checks if there's an answer after the current one on its first line, and if there is, applies a class of bordered to that LI tag. The very last answer won't get this class, allowing us to achieve the visual effect intended.


Step 9: The voteLine() Method

This method is getting 3 parameters passed into it:

  • $answer : The question answer for this line
  • $result : The number of votes this option has gotten
  • $votes : The total number of votes cast in this poll

With that information, the LI tags for the voting results can be produced.

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;'>&nbsp;</div>{$percent}%
            <label class='poll_results'>
                $answer
            </label>
    </li>
";
}

Since it's possible for there to be no votes for an option, it will effectively leave $result unset. If we detect this we'll give it a default value of 0 votes.

Next, we determine what percent of the votes the option got and finally use the scale property to determine the width, in pixels, that the results bar should be. Then we finally return the HTML containing all that information.


Step 10: Write the getData() Method

If you look back up a bit, you'll see we call the getData() method which is defined as a static method in the class. Why static? Because if we decide to enhance this poll later by making it AJAX based, we'll want access to that method without object creation. Here's the method:

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;
}

The question ID is passed into the method and it will return an array containing the answer ID's and the number of votes that answer has. If an answer has no votes, it won't have an entry in the array, which we've already dealt with in the voteLine() method.

Since database errors in web polls are particularly tragic, we're simply going to return an empty array if one occurs. The user will get 0 votes for each result. In a production environment you might want to log this error to a file, or send the admin an email.


Step 11: Handling a Vote

We're going to add a second static method to the class, and this one will handle incoming votes. Votes will only be counted if the user hasn't voted before (as determined by a cookie) and once the user has voted we'll set a cookie indicating this.

In this type of web application, it's nearly impossible to stop multiple votes without excluding some legitimate users. Setting a cookie is just a basic precaution.

This is one of the more complex methods in our webPoll class, and we're going to look at it in three parts.

static function vote() {
    if(!isset($_POST['QID']) ||
       !isset($_POST['AID']) ||
       isset($_COOKIE[$_POST['QID']])) {
        return;
    }

A call to the vote() method will be at the top of our PHP page, so the first thing we want to do is decide if there's a vote to process or not. The above statement is how we determine this. Here's what it says:

  • If there's no Question Identifier in our POST data (OR!!)
  • If there's no Answer Identifier in our POST data (OR!!)
  • If a cookie has been set already matching the Question Identifier

If any of those are true, we don't have to process a vote, and we leave the method.

$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());
    }
}

This looks a lot more complicated then it really is. What happens here is we check if a particular answer has gotten a vote before. If it hasn't we create a new record for that answer, and give it one vote. If it has, we update the existing record. So how's it decide which to do?

PDO exception magic.

Remember at the very beginning we created our multi-column primary key? When we try to insert a record into the table which matches an existing QID/AID pair, an exception is thrown, and in particular the exception code is 23000 (duplicate key).

If the insert throws an exception, we're going to check the exception code, and if it matches 23000, we'll try to update the record instead. Of course if the insert fails for a different reason, or the update fails as well, we're going to just issue a call to a method called db_error() which just echos a generic error message. Like before, a production environment would log this error and/or notify the admin.

Using PDO Exceptions

Finally, the end of the method:

    # 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;
    }
}

By using rowCount() we can verify that we've either updated or inserted a vote. If a vote has been successfully registered we set a cookie indicating as much, using the question identifier as the cookie name.

In addition to setting the cookie, we populate the super-global $_COOKIE, so when the poll displays, it shows answers rather then presenting the poll again.


Step 12: Put It All Into Action

We've written the PHP, set up the CSS and HTML, now it's time to put it all into use. In this example, we're just going to drop everything into a page that's otherwise blank. At the very top of the page, insert the following:

<?php
    include('webPoll.class.php');
    webPoll::vote();
?>

It's important that this be the very top of the page, before any HTML. Why? Because if there's a vote to process, a cookie may be written, and you can't write cookies after anything else has been sent. The call to the static method vote() returns if there's not the proper POST data to process.

Next, we'll include all the styles we wrote as a seperate stylesheet. Also, we're going to include a particular style just for IE that was mentioned earlier to enable the :hover psudo-class.

<link rel="stylesheet" href="poll.css" type="text/css" />
<!--[if IE]>
<style> body { behavior: url("res/hover.htc"); } </style>
<![endif]-->

In the BODY of your HTML page, you'll drop in the following PHP to insert the polls:

$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?'));

That's it! Thanks for reading. Any thoughts, questions, or suggestions?