Exchanging Information with Users: Putting It All Together in PHP

Turning the humble web form into a feature-packed application with data validation, printing default values, and processing the submitted results might seem like an intimidating task. To ease your burden, this section contains a complete example of a program that does it all:

  • Displaying a form, including default values
  • Validating the submitted data
  • Redisplaying the form with error messages and preserved user input if the sub­mitted data isn’t valid
  • Processing the submitted data if it is valid

The do-it-all example relies on a class containing some helper functions to simplify form element display and processing. This class is listed in Example 7-29.

Example 7-29. Form element display helper class

class FormHelper {

protected $values = array();

public function  construct($vatues = array()) {

if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’) {

$this->vatues = $_POST;

} else {

$this->values = $values;

}

}

public function input($type, $attributes = array(), $isMuttipte = false) {

$attributes[‘type’] = $type;

if (($type == ‘radio’) || ($type == ‘checkbox’)) {

if ($this->isOptionSelected($attributes[‘name’] ?? null,

$attributes[‘value’] ?? null)) {

$attributes[‘checked’] = true;

}

}

return $this->tag(‘input’, $attributes, $isMuttipte);

}

public function select($options, $attributes = array()) {

$muttipte = $attributes[‘muttipte’] ?? false;

return

$this->start(‘select’, $attributes, $muttipte) .

$this->options($attributes[‘name’] ?? null, $options) .

$this->end(‘select’);

}

public function textarea($attributes = array()) {

$name = $attributes[‘name’] ?? null;

$value = $this->values[$name] ?? ”;

return $this->start(‘textarea’, $attributes) .

   htmtentities($vatue) .

   $this->end(‘textarea’);

}

public function tag($tag, $attributes = array(), $isMuttipte = false) {

return “<$tag {$this->attributes($attributes, $isMuttipte)} />”;

}

public function start($tag, $attributes = array(), $isMuttipte = false) {

// <select> and <textarea> tags don’t get value attributes on them

$valueAttribute = (! (($tag == ‘select’)||($tag == ‘textarea’)));

$attrs = $this->attributes($attributes, $isMultiple, $valueAttribute);

return “<$tag $attrs>”;

}

public function end($tag) {

return “</$tag>”;

}

protected function attributes($attributes, $isMultiple,

  $valueAttribute = true) {

$tmp = array();

// If this tag could include a value attribute and it

// has a name and there’s an entry for the name

// in the values array, then set a value attribute

if ($valueAttribute && isset($attributes[‘name’]) &&

array_key_exists($attributes[‘name’], $this->values)) {

$attributes[‘value’] = $this->values[$attributes[‘name’]];

}

foreach ($attributes as $k => $v) {

// True boolean value means boolean attribute

if (is_bool($v)) {

if ($v) { $tmp[] = $this->encode($k); }

}

// Otherwise k=v else {

$value = $this->encode($v);

// If this is an element that might have multiple values,

// tack [] onto its name

if ($isMultiple && ($k == ‘name’)) {

$value .= ‘[]’;

}

$tmp[] = “$k=\”$value\””;

}

}

return implode(‘ ‘, $tmp);

}

protected function options($name, $options) {

$tmp = array();

foreach ($options as $k => $v) {

$s = “<option value=\”{$this->encode($k)}\””;

if ($this->isOptionSelected($name, $k)) {

$s .= ‘ selected’;

}

$s .= “>{$this->encode($v)}</option>”;

$tmp[] = $s;

}

return implode(”, $tmp);

}

protected function isOptionSelected($name, $value) {

// If there’s no entry for $name in the values array,

// then this option can’t be selected

if (! isset($this->values[$name])) {

return false;

}

// If the entry for $name in the values array is itself

// an array, check if $value is in that array

else if (is_array($this->values[$name])) {

return in_array($value, $this->values[$name]);

}

// Otherwise, compare $value to the entry for $name

// in the values array

else {

return $value == $this->values[$name];

}

}

public function encode($s) {

return htmlentities($s);

}

}

Methods in Example 7-29 incorporate the appropriate logic discussed in “Displaying Default Values” on page 142 for particular kinds of form elements. Because the form code in Example 7-30 has a number of different elements, it’s easier to put the ele­ment display code in functions that are called repeatedly than to duplicate the code each time you need to print a particular element.

The FormHelper constructor should be passed an associative array of default values for arguments. If the request method is not POST, it uses this array to figure out appropriate defaults. Otherwise, it uses the submitted data as the basis for defaults.

FormHelper’s inputQ method generates appropriate HTML for any <input/> ele­ment. Its required first argument is the type of the element (such as submit, radio, or text). The optional second argument is an associative array of element attributes (such as [‘ name’ => ‘meal’ ]). The optional third argument should be true if you’re generating HTML for an element that can have multiple values, such as a checkbox.

The select() method generates HTML for a <select> menu. Its first argument is an array of options for the menu and its optional second argument is an associative array of attributes for the <select> tag. For a multivalued <select> menu, make sure to include ‘multiple’ => true in the array of attributes passed as the second argument.

The textarea() method generates HTML for a <textarea>. It just takes a single argument: an associative array of attributes for the tag.

Those three methods should take care of the majority of your form display needs, but in case you need other tags or special treatment, you can use the tag(), start(), and end() methods.

The tag() method produces HTML for an entire self-closing HTML tag such as <input/>. Its arguments are the name of the tag, an optional array of attributes, and true if the tag can accept multiple values. The input() method uses tag() to actually generate the proper HTML.

The start() and end() methods are for elements with separate start and end tags. The start() method generates the element start tag, accepting the familiar trio of tag name, attributes, and multiple flag as arguments. The end() method just accepts a tag name for an argument and returns the closing tag HTML. For example, if you’re using an HTML tag such as <fieldset>, you could call start(‘fieldset’,[‘name’ => ‘ adjustments’ ]), then emit HTML that should be inside the field set, then call end(‘fieldset’).

The rest of the class is devoted to methods that help to generate the HTML and are not meant to be called from outside the class. The attributes() method formats a set of attributes to be appropriately included inside an HTML tag. Using the defaults set up in the object, it inserts an appropriate value attribute when necessary. It also takes care of appending [] to the element name if the element can accept multiple values and assures that all attribute values are appropriately encoded with HTML entities.

The options() method handles formatting the <option> tags for a <select> menu. With the help of isOptionSelected(), it figures out which options should be marked as selected and does proper HTML entity encoding.

The encode() method is a wrapper for PHP’s built-in htmlentities() method. It’s public so that other code can use it to make your entity encoding consistent.

The code in Example 7-30 relies on the FormHelper class and displays a short food­ordering form. When the form is submitted correctly, it shows the results in the browser and emails them to an address defined in process_form() (presumably to the chef, so he can start preparing your order). Because the code jumps in and out of PHP mode, it includes the <?php start tag at the beginning of the example and the ?> closing tag at the end to make things clearer.

Example 7-30. A complete form: display with defaults, validation, and processing

<?php

// This assumes FormHelper.php is in the same directory as

// this file.

require ‘FormHelper.php’;

// Set up the arrays of choices in the select menus.

// These are needed in display_form(), validate_form(),

// and process_form(), so they are declared in the global scope.

$sweets = array(‘puff’ => ‘Sesame Seed Puff’,

  ‘square’ => ‘Coconut Milk Gelatin Square’,

  ‘cake’ => ‘Brown Sugar Cake’,

  ‘ricemeat’ => ‘Sweet Rice and Meat’);

$main_dishes = array(‘cuke’ => ‘Braised Sea Cucumber’,

  ‘stomach’ => “Sauteed Pig’s Stomach”,

  ‘tripe’ => ‘Sauteed Tripe with Wine Sauce’,

  ‘taro’ => ‘Stewed Pork with Taro’,

  ‘giblets’ => ‘Baked Giblets with Salt’,

  ‘abalone’ => ‘Abalone with Marrow and Duck Feet’);

// The main page logic:

// – If the form is submitted, validate and then process or redisplay

// – If it’s not submitted, display

if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’) {

// If validate_form() returns errors, pass them to show_form()

list($errors, $input) = validate_form();

if ($errors) {

show_form($errors);

} else {

// The submitted data is valid, so process it

process_form($input);

}

} else {

// The form wasn’t submitted, so display

show_form();

}

function show_form($errors = array()) {

$defaults = array(‘delivery’ => ‘yes’,

‘size’ => ‘medium’);

// Set up the $form object with proper defaults

$form = new FormHelper($defaults);

// All the HTML and form display is in a separate file for clarity

include ‘complete-form.php’;

}

function validate_form() {

$input = array();

$errors = array();

// name is required

$input[‘name’] = trim($_POST[‘name’] ?? ”);

if (! strlen($input[‘name’])) {

$errors[] = ‘Please enter your name.’;

}

// size is required

$input[‘size’] = $_POST[‘size’] ?? ”;

if (! in_array($input[‘size’], [‘small’,’medium’,’large’])) {

$errors[] = ‘Please select a size.’;

}

// sweet is required

$input[‘sweet’] = $_POST[‘sweet’] ?? ”;

if (! array_key_exists($input[‘sweet’], $GLOBALS[‘sweets’])) {

$errors[] = ‘Please select a valid sweet item.’;

}

// exactly two main dishes required

$input[‘main_dish’] = $_POST[‘main_dish’] ?? array();

if (count($input[‘main_dish’]) !=      ) {

$errors[] = ‘Please select exactly two main dishes.’;

} else {

// we know there are two main dishes selected, so make sure they are

// both valid

if (! (array_key_exists($input[‘main_dish’][ ], $GLOBALS[‘main_dishes’]) &&

  array_key_exists($input[‘main_dish’][ ], $GLOBALS[‘main_dishes’]))) {

$errors[] = ‘Please select exactly two valid main dishes.’;

}

}

// if delivery is checked, then comments must contain something

$input[‘delivery’] = $_POST[‘delivery’] ?? ‘no’;

$input[‘comments’] = trim($_POST[‘comments’] ?? ”);

if (($input[‘delivery’] == ‘yes’) && (! strlen($input[‘comments’]))) {

$errors[] = ‘Please enter your address for delivery.’;

}

return array($errors, $input);

}

function process_form($input) {

// look up the full names of the sweet and the main dishes in

// the $GLOBALS[‘sweets’] and $GLOBALS[‘main_dishes’] arrays

$sweet = $GLOBALS[‘sweets’][ $input[‘sweet’] ];

$main_dish_1 = $GLOBALS[‘main_dishes’][ $input[‘main_dish’][ ] ];

$main_dish_2 = $GLOBALS[‘main_dishes’][ $input[‘main_dish’][ ] ];

if (isset($input[‘delivery’]) && ($input[‘delivery’] == ‘yes’)) {

$delivery = ‘do’;

} else {

$delivery = ‘do not’;

}

// build up the text of the order message

$message=<<<_ORDER_

Thank you for your order, {$input[‘name’]}.

You requested the {$input[‘size’]} size of $sweet, $main_dish_1, and $main_dish_2.

You $delivery want delivery.

_ORDER_;

if (strlen(trim($input[‘comments’]))) {

$message .= ‘Your comments: ‘.$input[‘comments’];

}

// send the message to the chef

mail(‘chef@restaurant.example.com’, ‘New Order’, $message);

// print the message, but encode any HTML entities

// and turn newlines into <br/> tags

print nl2br(htmlentities($message, ENT_HTML5));

}

?>

There are four parts to the code in Example 7-30: the code in the global scope at the top of the example, the show_form() function, the validate_fomO function, and the process_form() function.

The global scope code does three things. The first is that it loads the FormHelper class from its separate file. Then, it sets up two arrays that describe the choices in the form’s two <select> menus. Because these arrays are used by each of the show_fom(), validate_form(), and process_form() functions, they need to be defined in the global scope. The global code’s last task is to process the if() statement that decides what to do: display, validate, or process the form.

Displaying the form is accomplished by show_fom(). First, the function makes $defaults an array of default values. This array is passed to FormHelper’s construc­tor, so the $form object uses the right default values. Then, show_fom() hands off control to another file, complete-form.php, which contains the actual HTML and PHP code to display the form. Putting the HTML in a separate file for a big program like this makes it easier to digest everything and also easier for the two files to be changed independently. The contents of complete-form.php are shown in Example 7-31.

Example 7-31. PHP and HTML generating a form

form method=”POST” action=”<?= $form->encode($_SERVER[‘PHP_SELF’]) ?>”>

<table>

<?php if ($errors) { ?>

<tr>

<td>You need to correct the following errors:</td>

<td><ul>

<?php foreach ($errors as $error) { ?>

<li><?= $form->encode($error) ?></li>

<?php } ?>

</ul></td>

<?php }   ?>

<tr><td>Your Name:</td><td><?= $form->input(‘text’, [‘name’ => ‘name’]) ?>

</td></tr>

<tr><td>Size:</td>

<td><?= $form->input(‘radio’,[‘name’ => ‘size’, ‘value’ => ‘small’]) ?>

Small <br/>

<?= $form->input(‘radio’,[‘name’ => ‘size’, ‘value’ => ‘medium’]) ?>

Medium <br/>

<?= $form->input(‘radio’,[‘name’ => ‘size’, ‘value’ => ‘large’]) ?>

Large <br/>

</td></tr>

<tr><td>Pick one sweet item:</td>

<td><?= $form->select($GLOBALS[‘sweets’], [‘name’ => ‘sweet’]) ?></td>

</tr>

<tr><td>Pick two main dishes:</td>

<td><?= $form->select($GLOBALS[‘main_dishes’], [‘name’ => ‘main_dish’,

  ‘multiple’ => true]) ?></td>

</tr>

<tr><td>Do you want your order delivered?</td>

<td><?= $form->input(‘checkbox’,[‘name’ => ‘delivery’,

 ‘value’ => ‘yes’])

?> Yes </td></tr>

<tr><td>Enter any special instructions.<br/>

If you want your order delivered, put your address here:</td>

<td><?= $form->textarea([‘name’ => ‘comments’]) ?></td></tr>

<tr><td colspan=”2″ align=”center”>

<?=$form->input(‘submit’, [‘value’ => ‘Order’]) ?>

</td></tr>

</table>

</form>

The code in complete-form.php executes as if it were part of the show_form() func­tion. This means that local variables in the function, such as $errors and $form, are available in complete-form.php. Like all included files, complete-form.php starts out­side of any PHP tags, so it can print some plain HTML and then jump into PHP mode when it needs to call methods or use PHP logic. The code here uses the special short echo tag (<?=) as a concise way to display the results of various method calls. Starting a PHP block with <?= means exactly the same thing as starting a PHP block with <php echo. Since our various FormHelper methods return HTML that should be displayed, this makes a handy way to build up the HTML for the form.

Back in the main file, the validate_form() function builds an array of error mes­sages if the submitted form data doesn’t meet appropriate criteria. Note that the checks for size, sweet, and main_dish don’t just look to see whether something was submitted for those parameters, but also that what was submitted is a valid value for the particular parameter. For size, this means that the submitted value must be small, medium, or large. For sweet and main_dish, this means that the submitted values must be keys in the global $sweets or $main_dishes arrays. Even though the form contains default values, it’s still a good idea to validate the input. Someone try­ing to break into your website could bypass a regular web browser and construct a request with an arbitrary value that isn’t a legitimate choice for the <select> menu or radio button set.

Lastly, process_fom() takes action when the form is submitted with valid data. It builds a string, $message, that contains a description of the submitted order. Then it emails $message to chef@restaurant.example.com and prints it. The built-in mail() function sends the email message. Before printing $message, process_form() passes it through two functions. The first is htmlentities(), which, as you’ve already seen, encodes any special characters as HTML entities. The second is nl2br(), which turns any newlines in $message into HTML <br> tags. Turning newlines into <br> tags makes the line breaks in the message display prop­erly in a web browser.

Source: Sklar David (2016), Learning PHP: A Gentle Introduction to the Web’s Most Popular Language, O’Reilly Media; 1st edition.

Leave a Reply

Your email address will not be published. Required fields are marked *