A Poll: Arrays

A common thing to do on a poll after someone votes is show them the current results. We can do that, but to do it we need a list of all of the options available. In PHP, a list is called an array. For example, if you want to keep a list of colors, you might have an array that contains “red”, “blue”, “green”, “cyan”, “yellow”, and “magenta”. The simplest arrays are just lists of values.

Arrays can also be more complex. For example, you might have a list of employee identification numbers and employee names and phone numbers. If so, you could take the employee identification number and use that to look up an employee’s name and the employee’s phone number.

What we’re going to do is first read the entire file into a simple array that lists every line in the file. Then we’re going to ask PHP to count all of the similar entries. PHP will return that count in a slightly more complex array that corresponds each unique entry with the number of times that entry appears.

If fourteen people have voted for the Tin Man and three people have voted for Neo, the simple array will contain fourteen tinmans and three neos. The count will contain one “tinman” and correspond that to “14”, and one “neo” that corresponds to “3”.

Add this method to VoteCounter:

public function counts() {

$votes = file($this->filePath);

$votecounts = array_count_values($votes);

return $votecounts;

}

First, we read our ballots using the “file” command. It takes every line of the file and places each line as an entry in the array $votes. We then use the function array_count_values() to count the items in the $votes array. That result is stored in $votecounts.

If there are five votes, with one for the Scarecrow, two for the Tin Man, and two for Neo, $votes will contain something like “scarecrow, tinman, neo, neo, tinman”. The variable $votecounts will contain a table of values:

scarecrow

1

tinman

2

neo

2

In the web page, add a table below the thanks for voting message:

<?php IF ($vote->answered()):?>

<p>Thank you for voting!</p>

<p>The current results are:</p>

<table>

<?php FOREACH ($vote->counts() as $choice=>$count):?>

<tr>

<th><?php echo $choice; ?></th>

<td><?php echo $count; ?></td>

</tr>

<?php ENDFOREACH;?>

</table>

<?php ELSE:?>

The foreach structure is a lot like the if structure. Like IF, the lines between the FOREACH and the ENDFOREACH are performed while the foreach is valid. Unlike the if, however, the lines enclosed by a foreach block can, and usually are, performed more than once. PHP goes through those lines once for every entry in the array. If there are five items in the array, PHP will use those lines five times. In between the parentheses, we give foreach the array and the name of the variable(s) that should contain the current array item each time we go through the “loop”.

In the $votecounts array, each item is composed of two parts: the choice, and the count of how many times that choice was chosen. So when we say “$votecounts as $choice => $count”, we’re telling PHP to give us the first part of the array item in $choice and the second part in $count. If there were only one part (such as the $votes array), we would use something like “$votes as $choice”.

You may want to add a new style to the page, to align <th> cells to the right:

th {

text-align: right;

}

That’ll produce output like this:

Accessing array data

The above code works. It accepts votes and it shows the results of the votes. But the results are ugly. We’re showing the visitors our internal representation of the votes, not the human representation. Instead of displaying “The White Rabbit” for example, we’re displaying “rabbit”. We need an array to store our internal representation and correspond it to our English representation.

Add a third parameter to the VoteCounter __construct method:

protected $ballotFolder = '/home/USERNAME/ballots/';

protected $filePath;

protected $choices;

public function __construct($fieldName, $choices) {

$this->choices = $choices;

parent::__construct($fieldName);

$this->filePath = $this->ballotFolder . $fieldName . '.txt';

}

On the web page, add this an array of the vote values:

include_once('/home/USERNAME/includes/fields.phpi');

$imaginaries = array(

'rabbit'=>'The White Rabbit',

'scarecrow'=>'The Scarecrow',

'tinman'=>'The Tin Man',

'neo'=>'Neo',

);

$vote = new VoteCounter('character', $imaginaries);

$vote->save();

The values in the array are the same as the values in the select menu. In the results table, change the FOREACH that creates the table.

th><?php echo $vote->choiceName($choice); ?></th>

And add the choiceName method to VoteCounter:

public function choiceName($choice) {

return $this->choices[$choice];

}

What this is supposed to do is look up, say, “tinman” in the array of imaginary characters and get back “The Tin Man”. What it actually provides is a whole bunch of undefined indexes:

If you don’t see undefined indexes, make sure you have error display turned on. See Finding Errors.

That doesn’t look like it makes any sense. One thing to always look at when you see unexplained errors, however, is the source HTML of the page. In the browser, do a “view source” to see the HTML that PHP is generating. In this case, you’ll see that there’s a carriage return after each item. The keys scarecrow, neo, tinman, and rabbit all have a carriage return after them.

It turns out that when the file function reads lines in from a file, it doesn’t throw out the carriage return at the end of each line. A quick look at the php.net web page for the file function shows that there’s a special parameter to make file throw out the end of line character. Modify the counts() method to use this parameter.

$votes = file($this->filePath, FILE_IGNORE_NEW_LINES);

And now we’ve got it.

Arrays: Sorting

Once you store values in an array, you can do all sorts of automatic things on them. For example, we can sort arrays using “asort()” and “arsort()”. The first sorts in ascending order, the second in descending (reverse) order. Why not show our results in order from the most popular on down? In the VoteCounter’s counts method, add a new line:

public function counts() {

$votes = file($this->filePath, FILE_IGNORE_NEW_LINES);

$votecounts = array_count_values($votes);

arsort($votecounts);

return $votecounts;

}

Our display will now automatically adjust itself according to who is winning the poll.

Keep your powder D-R-Y

Keep your code organized and, as best as you can, in a central location. One of the cardinal rules of programming is D-R-Y: Don’t Repeat Yourself. We are now, however, repeating ourselves. If we wanted to add a new contestant to the imaginary character list, we would have to do it in two places: once for the VoteCounter class, and once in the select menu in the page form. We’re also repeating the field name. If we change the field name later, we need to remember to do it in two places. Chances are, we’ll forget.

Replace the entire select menu with a method:

<p>

Please choose your favorite imaginary imaginary character:

<?php echo $vote->formSelect(); ?>

<input type="submit" value="Submit your answer" />

</p>

Inside the VoteCounter class, add a new method called formSelect:

public function formSelect() {

echo '<select name="', $this->fieldName, '">';

echo '<option value="">Choose:</option>';

foreach ($this->choices as $key=>$name) {

echo '<option value="', $key, '">', $name, '</option>';

}

echo '</select>';

}

This constructs the same select menu using the choices property. On the page, the HTML should look exactly the same.

Don’t trust anyone

It’s not paranoia when they really are out to get you. Remember, never trust anything coming from outside of your own PHP code. It’s important to think, not in terms of what it looks like is happening but what is actually happening. What it looks like is happening is that visitors come to our form, select a choice, and then submit that choice. Then we write that choice to a file to record.

That’s not what’s happening. What’s happening is that they are sending us text, and we are writing that text, whatever it is, to a file on our server. If they send us text that includes a computer program, we will write that computer program to a file on our server! And the form in their browser tells them exactly how to do that. That’s very, very bad.

We no longer have to trust them. We have a list of valid choices. We can verify their submission against that list.

There’s already a method, on FormField, that cleans submissions. Override that method to check against the array of choices:

protected function clean() {

parent::clean();

if (isset($this->value)) {

//if their vote isn’t in the list of valid votes, cancel

if (!isset($this->choices[$this->value])) {

$this->value = '';

}

}

}

First, it calls the parent to perform basic cleaning. Then it verifies that a choice for the submitted value exists (“is set”) in the choices property. The exclamation point reverses the check, so what we’re checking is if the value does not exist. If it doesn’t, it sets $this->vote to '' so that we don’t use it.

Test this by temporarily disabling one of your array options: First, load the page, so that you have all of the options available in the menu. Then put two slashes in front of the White Rabbit’s line and upload the page again. Then submit. You should be told that you need to answer the question as if you hadn’t answered it. Your submissions are now being ignored if they’re not in the list.

Lost?

“Aye indeed. I keep it at the bottom of my backpack and take it out to shine it up and look at it on windy nights in the wilderness, by the fire. It looks grand, I tell you. But it is poor company, and doesn’t keep one warm.” — Torm, Knight of Myth Drannor