Appendix B. Answers to Exercises

Chapter 2

Exercise 1

  1. The opening PHP tag should be just <?php with no space between the <? and php.
  2. Because the string I'm fine contains a ', it should be surrounded by double quotes ("I'm fine") or the ' should be escaped ('I\'m fine').
  3. The closing PHP tag should be ?>, not ??>. Or, if this code were the last thing in its file, the closing PHP tag could be omitted.

Exercise 2

$hamburger = 4.95;
$shake = 1.95;
$cola = 0.85;

$tip_rate = 0.16;
$tax_rate = 0.075;

$food = (2 * $hamburger) + $shake + $cola;
$tip = $food * $tip_rate;
$tax = $food * $tax_rate;

$total = $food + $tip + $tax;

print 'The total cost of the meal is $' . $total;

Exercise 3

$hamburger = 4.95;
$shake = 1.95;
$cola = 0.85;

$tip_rate = 0.16;
$tax_rate = 0.075;

$food = (2 * $hamburger) + $shake + $cola;
$tip = $food * $tip_rate;
$tax = $food * $tax_rate;

$total = $food + $tip + $tax;

printf("%d %-9s at \$%.2f each: \$%5.2f\n", 2, 'Hamburger', $hamburger,
       2 * $hamburger);
printf("%d %-9s at \$%.2f each: \$%5.2f\n", 1, 'Shake', $shake, $hamburger);
printf("%d %-9s at \$%.2f each: \$%5.2f\n", 1, 'Cola', $cola, $cola);
printf("%25s: \$%5.2f\n", 'Food Total', $food);
printf("%25s: \$%5.2f\n", 'Food and Tax Total', $food + $tax);
printf("%25s: \$%5.2f\n", 'Food, Tax, and Tip Total', $total);

Exercise 4

$first_name = 'Srinivasa';
$last_name = 'Ramanujan';
$name = "$first_name $last_name";
print $name;
print strlen($name);

Exercise 5

$n = 1; $p = 2;
print "$n, $p\n";

$n++; $p *= 2;
print "$n, $p\n";

$n++; $p *= 2;
print "$n, $p\n";

$n++; $p *= 2;
print "$n, $p\n";

$n++; $p *= 2;
print "$n, $p\n";

Chapter 3

Exercise 1

  1. false
  2. true
  3. true
  4. false
  5. false
  6. true
  7. true
  8. false

Exercise 2

Message 3.Age: 12. Shoe Size: 14

Exercise 3

$f = -50;
while ($f <= 50) {
    $c = ($f - 32) * (5/9);
    printf("%d degrees F = %d degrees C\n", $f, $c);
    $f += 5;
}

Exercise 4

for ($f = -50; $f <= 50; $f += 5) {
    $c = ($f - 32) * (5/9);
    printf("%d degrees F = %d degrees C\n", $f, $c);
}

Chapter 4

Exercise 1

<table>
<tr><th>City</th><th>Population</th></tr>
<?php
$census = ['New York, NY' => 8175133,
           'Los Angeles, CA' => 3792621,
           'Chicago, IL' => 2695598,
           'Houston, TX' => 2100263,
           'Philadelphia, PA' => 1526006,
           'Phoenix, AZ' => 1445632,
           'San Antonio, TX' => 1327407,
           'San Diego, CA' => 1307402,
           'Dallas, TX' => 1197816,
           'San Jose, CA' => 945942];

$total = 0;
foreach ($census as $city => $population) {
    $total += $population;
    print "<tr><td>$city</td><td>$population</td></tr>\n";
}
print "<tr><td>Total</td><td>$total</td></tr>\n";
print "</table>";

Exercise 2

$census = ['New York, NY' => 8175133,
           'Los Angeles, CA' => 3792621,
           'Chicago, IL' => 2695598,
           'Houston, TX' => 2100263,
           'Philadelphia, PA' => 1526006,
           'Phoenix, AZ' => 1445632,
           'San Antonio, TX' => 1327407,
           'San Diego, CA' => 1307402,
           'Dallas, TX' => 1197816,
           'San Jose, CA' => 945942];

// Sort the associative array by value
asort($census);

print "<table>\n";
print "<tr><th>City</th><th>Population</th></tr>\n";
$total = 0;
foreach ($census as $city => $population) {
    $total += $population;
    print "<tr><td>$city</td><td>$population</td></tr>\n";
}
print "<tr><td>Total</td><td>$total</td></tr>\n";
print "</table>";

// Sort the associative array by key
ksort($census);

print "<table>\n";
print "<tr><th>City</th><th>Population</th></tr>\n";
$total = 0;
foreach ($census as $city => $population) {
    $total += $population;
    print "<tr><td>$city</td><td>$population</td></tr>\n";
}
print "<tr><td>Total</td><td>$total</td></tr>\n";
print "</table>";

Exercise 3

<table>
<tr><th>City</th><th>Population</th></tr>
<?php
// Each element in $census is a three-element array
// containing city name, state, and population
$census = [ ['New York', 'NY', 8175133],
            ['Los Angeles', 'CA' , 3792621],
            ['Chicago', 'IL' , 2695598],
            ['Houston', 'TX' , 2100263],
            ['Philadelphia', 'PA' , 1526006],
            ['Phoenix', 'AZ' , 1445632],
            ['San Antonio', 'TX' , 1327407],
            ['San Diego', 'CA' , 1307402],
            ['Dallas', 'TX' , 1197816],
            ['San Jose', 'CA' , 945942] ];

$total = 0;
$state_totals = array();
foreach ($census as $city_info) {
    // Update the total population
    $total += $city_info[2];
    // If we haven't seen this state yet, initialize its
    // population total to 0
    if (! array_key_exists($city_info[1], $state_totals)) {
        $state_totals[$city_info[1]] = 0;
    }
    // Update the per-state population
    $state_totals[$city_info[1]] += $city_info[2];
    print "<tr><td>$city_info[0], $city_info[1]</td><td>
        $city_info[2]</td></tr>\n";
}
print "<tr><td>Total</td><td>$total</td></tr>\n";
// Print the per-state totals
foreach ($state_totals as $state => $population) {
    print "<tr><td>$state</td><td>$population</td></tr>\n";
}
print "</table>";

Exercise 4

/* The grades and ID numbers of students in a class:
   An associative array whose key is the student's name and whose value is
   an associative array of grade and ID number
*/
$students = [ 'James D. McCawley' => [ 'grade' => 'A+','id' => 271231 ],
              'Buwei Yang Chao' => [ 'grade' => 'A', 'id' => 818211] ];

/* How many of each item in a store inventory are in stock:
   An associative array whose key is the item name and whose value is the
   number in stock
 */
$inventory = [ 'Wok' => 5, 'Steamer' => 3, 'Heavy Cleaver' => 3,
               'Light Cleaver' => 0 ];

/* School lunches for a week — the different parts of each meal
   (entree, side dish, drink, etc.) and the cost for each day:
   An associative array whose key is the day and whose value is an
   associative array describing the meal. This associative array has a key/value
   pair for cost and a key/value pair for each part of the meal.
*/
$lunches = [ 'Monday' => [ 'cost' => 1.50,
                           'entree' => 'Beef Shu-Mai',
                           'side' => 'Salty Fried Cake',
                           'drink' => 'Black Tea' ],
             'Tuesday' => [ 'cost' => 2.50,
                           'entree' => 'Clear-steamed Fish',
                           'side' => 'Turnip Cake',
                           'drink' => 'Bubble Tea' ],
             'Wednesday' => [ 'cost' => 2.00,
                           'entree' => 'Braised Sea Cucumber',
                           'side' => 'Turnip Cake',
                           'drink' => 'Green Tea' ],
             'Thursday' => [ 'cost' => 1.35,
                           'entree' => 'Stir-fried Two Winters',
                           'side' => 'Egg Puff',
                           'drink' => 'Black Tea' ],
             'Friday' => [ 'cost' => 3.25,
                           'entree' => 'Stewed Pork with Taro',
                           'side' => 'Duck Feet',
                           'drink' => 'Jasmine Tea' ] ];


/* The names of people in your family:
   A numeric array whose indices are implicit and whose values are the names
   of family members
 */
$family = [ 'Bart', 'Lisa', 'Homer', 'Marge', 'Maggie' ];

/* The names, ages, and relationship to you of people in your family:
   An associative array whose keys are the names of family members and whose
   values are associative arrays with age and relationship key/value pairs
 */
$family = [ 'Bart' => [ 'age' => 10,
                        'relation' => 'brother' ],
            'Lisa' => [ 'age' => 7,
                        'relation' => 'sister' ],
            'Homer' => [ 'age' => 36,
                        'relation' => 'father' ],
            'Marge' => [ 'age' => 34,
                        'relation' => 'mother' ],
            'Maggie' => [ 'age' => 1,
                        'relation' => 'self' ] ];

Chapter 5

Exercise 1

function html_img($url, $alt = null, $height = null, $width = null) {
    $html = '<img src="' . $url . '"';
    if (isset($alt)) {
        $html .= ' alt="' . $alt . '"';
    }
    if (isset($height)) {
        $html .= ' height="' . $height . '"';
    }
    if (isset($width)) {
        $html .= ' width="' . $width . '"';
    }
    $html .= '/>';
    return $html;
}

Exercise 2

function html_img2($file, $alt = null, $height = null, $width = null) {
    if (isset($GLOBALS['image_path'])) {
        $file = $GLOBALS['image_path'] . $file;
    }
    $html = '<img src="' . $file . '"';
    if (isset($alt)) {
        $html .= ' alt="' . $alt . '"';
    }
    if (isset($height)) {
        $html .= ' height="' . $height . '"';
    }
    if (isset($width)) {
        $html .= ' width="' . $width . '"';
    }
    $html .= '/>';
    return $html;
}

Exercise 3

// The html_img2() function from the previous exercise is saved in this file
include "html-img2.php";

$image_path = '/images/';

print html_img2('puppy.png');
print html_img2('kitten.png','fuzzy');
print html_img2('dragon.png',null,640,480);

Exercise 4

I can afford a tip of 11% (30)
I can afford a tip of 12% (30.25)
I can afford a tip of 13% (30.5)
I can afford a tip of 14% (30.75)

Exercise 5

/* Using dechex(): */
function web_color1($red, $green, $blue) {
    $hex = [ dechex($red), dechex($green), dechex($blue) ];
    // Prepend a leading 0 if necessary to 1-digit hex values
    foreach ($hex as $i => $val) {
        if (strlen($i) == 1) {
            $hex[$i] = "0$val";
        }
    }
    return '#' . implode('', $hex);
}

/* You can also rely on sprintf()'s %x format character to do
   hex-to-decimal conversion: */
function web_color2($red, $green, $blue) {
    return sprintf('#%02x%02x%02x', $red, $green, $blue);
}

Chapter 6

Exercise 1

class Ingredient {
    protected $name;
    protected $cost;

    public function __construct($name, $cost) {
        $this->name = $name;
        $this->cost = $cost;
    }

    public function getName() {
        return $this->name;
    }

    public function getCost() {
        return $this->cost;
    }
}

Exercise 2

class Ingredient {
    protected $name;
    protected $cost;

    public function __construct($name, $cost) {
        $this->name = $name;
        $this->cost = $cost;
    }

    public function getName() {
        return $this->name;
    }

    public function getCost() {
        return $this->cost;
    }

    // This method sets the cost to a new value
    public function setCost($cost) {
        $this->cost = $cost;
    }

}

Exercise 3

class PricedEntree extends Entree {
    public function __construct($name, $ingredients) {
        parent::__construct($name, $ingredients);
        foreach ($this->ingredients as $ingredient) {
            if (! $ingredient instanceof Ingredient) {
                throw new Exception('Elements of $ingredients must be
                Ingredient objects');
            }
        }
    }

    public function getCost() {
        $cost = 0;
        foreach ($this->ingredients as $ingredient) {
            $cost += $ingredient->getCost();
        }
        return $cost;
    }
}

Exercise 4

The Ingredient class in its own namespace:

namespace Meals;

class Ingredient {
    protected $name;
    protected $cost;

    public function __construct($name, $cost) {
        $this->name = $name;
        $this->cost = $cost;
    }

    public function getName() {
        return $this->name;
    }

    public function getCost() {
        return $this->cost;
    }

    // This method sets the cost to a new value
    public function setCost($cost) {
        $this->cost = $cost;
    }

}

The PricedEntree class referencing that namespace:

class PricedEntree extends Entree {
    public function __construct($name, $ingredients) {
        parent::__construct($name, $ingredients);
        foreach ($this->ingredients as $ingredient) {
            if (! $ingredient instanceof \Meals\Ingredient) {
                throw new Exception('Elements of $ingredients must be
                Ingredient objects');
            }
        }
    }

    public function getCost() {
        $cost = 0;
        foreach ($this->ingredients as $ingredient) {
            $cost += $ingredient->getCost();
        }
        return $cost;
    }
}

Chapter 7

Exercise 1

$_POST['noodle'] = 'barbecued pork';
$_POST['sweet'] = [ 'puff', 'ricemeat' ];
$_POST['sweet_q'] = '4';
$_POST['submit'] = 'Order';

Exercise 2

/* Since this is operating on form data, it looks directly at $_POST
   instead of a validated $input array */
function process_form() {
    print '<ul>';
    foreach ($_POST as $k => $v) {
        print '<li>' . htmlentities($k) .'=' . htmlentities($v) . '</li>';
    }
    print '</ul>';
}

Exercise 3

<?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 menu.
// This is needed in display_form(), validate_form(),
// and process_form(), so it is declared in the global scope.
$ops = array('+','-','*','/');

// 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);
        // And then show the form again to do another calculation
        show_form();
    }
} else {
    // The form wasn't submitted, so display
    show_form();
}

function show_form($errors = array()) {
    $defaults = array('num1' => 2,
                      'op' => 2, // the index of '*' in $ops
                      'num2' => 8);
    // 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 'math-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    // op is required
    $input['op'] = $GLOBALS['ops'][$_POST['op']] ?? '';
    if (! in_array($input['op'], $GLOBALS['ops'])) {
        $errors[] = 'Please select a valid operation.';
    }
    // num1 and num2 must be numbers
    $input['num1'] = filter_input(INPUT_POST, 'num1', FILTER_VALIDATE_FLOAT);
    if (is_null($input['num1']) || ($input['num1'] === false)) {
        $errors[] = 'Please enter a valid first number.';
    }

    $input['num2'] = filter_input(INPUT_POST, 'num2', FILTER_VALIDATE_FLOAT);
    if (is_null($input['num2']) || ($input['num2'] === false)) {
        $errors[] = 'Please enter a valid second number.';
    }

    // Can't divide by zero
    if (($input['op'] == '/') && ($input['num2'] == 0)) {
        $errors[] = 'Division by zero is not allowed.';
    }

    return array($errors, $input);
}

function process_form($input) {
    $result = 0;
    if ($input['op'] == '+') {
        $result = $input['num1'] + $input['num2'];
    }
    else if ($input['op'] == '-') {
        $result = $input['num1'] - $input['num2'];
    }
    else if ($input['op'] == '*') {
        $result = $input['num1'] * $input['num2'];
    }
    else if ($input['op'] == '/') {
        $result = $input['num1'] / $input['num2'];
    }
    $message = "{$input['num1']} {$input['op']} {$input['num2']} = $result";

    print "<h3>$message</h3>";
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The math-form.php file referenced, which displays the form HTML, contains:

<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>First Number:</td>
        <td><?= $form->input('text', ['name' => 'num1']) ?></td>
    </tr>
    <tr><td>Operation:</td>
        <td><?= $form->select($GLOBALS['ops'], ['name' => 'op']) ?></td>
    </tr>
    <tr><td>Second Number:</td>
        <td><?= $form->input('text', ['name' => 'num2']) ?></td>
    </tr>

    <tr><td colspan="2" align="center"><?= $form->input('submit', 
    ['value' => 'Calculate']) ?>
    </td></tr>

</table>
</form>

Exercise 4

<?php

// This assumes FormHelper.php is in the same directory as
// this file.
require 'FormHelper.php';

// Set up the array of choices in the select menu.
// This is needed in display_form(), validate_form(),
// and process_form(), so it is declared in the global scope.
$states = [ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA', 
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 
'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 
'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 
'WY' ];

// 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()) {
    // Set up the $form object with proper defaults
    $form = new FormHelper();

    // All the HTML and form display is in a separate file for clarity
    include 'shipping-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    foreach (['from','to'] as $addr) {
        // Check required fields
        foreach (['Name' => 'name', 'Address 1' => 'address1',
                  'City' => 'city', 'State' => 'state'] as $label => $field){
            $input[$addr.'_'.$field] = $_POST[$addr.'_'.$field] ?? '';
            if (strlen($input[$addr.'_'.$field]) == 0) {
                $errors[] = "Please enter a value for $addr $label.";
            }
        }
        // Check state
        $input[$addr.'_state'] = 
        $GLOBALS['states'][$input[$addr.'_state']] ?? '';
        if (! in_array($input[$addr.'_state'], $GLOBALS['states'])) {
            $errors[] = "Please select a valid $addr state.";
        }
        // Check zip code
        $input[$addr.'_zip'] = filter_input(INPUT_POST, $addr.'_zip',
                                            FILTER_VALIDATE_INT,
                                            ['options' => ['min_range'=>10000,
                                                          'max_range'=>99999]]);
        if (is_null($input[$addr.'_zip']) || ($input[$addr.'_zip']===false)) {
            $errors[] = "Please enter a valid $addr ZIP";
        }
        // Don't forget about address2!
        $input[$addr.'_address2'] = $_POST[$addr.'_address2'] ?? '';
   }

    // height, width, depth, weight must all be numbers > 0
    foreach(['height','width','depth','weight'] as $field) {
        $input[$field] =filter_input(INPUT_POST, $field, FILTER_VALIDATE_FLOAT);
        // Since 0 is not valid, we can just test for truth rather than
        // null or exactly false
        if (! ($input[$field] && ($input[$field] > 0))) {
            $errors[] = "Please enter a valid $field.";
        }
    }
    // Check weight
    if ($input['weight'] > 150) {
        $errors[] = "The package must weigh no more than 150 lbs.";
    }
    // Check dimensions
    foreach(['height','width','depth'] as $dim) {
        if ($input[$dim] > 36) {
            $errors[] = "The package $dim must be no more than 36 inches.";
        }
    }

    return array($errors, $input);
}

function process_form($input) {
    // Make a template for the report
    $tpl=<<<HTML
<p>Your package is {height}" x {width}" x {depth}" and weighs {weight} lbs.</p>

<p>It is coming from:</p>
<pre>
{from_name}
{from_address}
{from_city}, {from_state} {from_zip}
</pre>

<p>It is going to:</p>
<pre>
{to_name}
{to_address}
{to_city}, {to_state} {to_zip}
</pre>
HTML;

    // Adjust addresses in $input for easier output
    foreach(['from','to'] as $addr) {
        $input[$addr.'_address'] = $input[$addr.'_address1'];
        if (strlen($input[$addr.'_address2'])) {
            $input[$addr.'_address'] .= "\n" . $input[$addr.'_address2'];
        }
    }

    // Replace each template variable with the corresponding value
    // in $input
    $html = $tpl;
    foreach($input as $k => $v) {
        $html = str_replace('{'.$k.'}', $v, $html);
    }

    // Print the report
    print $html;
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The shipping-form.php file referenced, which displays the form HTML, contains:

<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><th>From:</th><td></td></tr>
    <tr><td>Name:</td>
        <td><?= $form->input('text', ['name' => 'from_name']) ?></td></tr>
    <tr><td>Address 1:</td>
        <td><?= $form->input('text', ['name' => 'from_address1']) ?></td></tr>
    <tr><td>Address 2:</td>
        <td><?= $form->input('text', ['name' => 'from_address2']) ?></td></tr>
    <tr><td>City:</td>
        <td><?= $form->input('text', ['name' => 'from_city']) ?></td></tr>
    <tr><td>State:</td>
        <td><?= $form->select($GLOBALS['states'], ['name' => 'from_state']) ?>
        </td></tr>
    <tr><td>ZIP:</td>
        <td><?= $form->input('text', ['name' => 'from_zip', 'size' => 5]) ?>
        </td></tr>

    <tr><th>To:</th><td></td></tr>
    <tr><td>Name:</td>
        <td><?= $form->input('text', ['name' => 'to_name']) ?></td></tr>
    <tr><td>Address 1:</td>
        <td><?= $form->input('text', ['name' => 'to_address1']) ?></td></tr>
    <tr><td>Address 2:</td>
        <td><?= $form->input('text', ['name' => 'to_address2']) ?></td></tr>
    <tr><td>City:</td>
        <td><?= $form->input('text', ['name' => 'to_city']) ?></td></tr>
    <tr><td>State:</td>
        <td><?= $form->select($GLOBALS['states'], ['name' => 'to_state']) ?>
        </td></tr>
    <tr><td>ZIP:</td>
        <td><?= $form->input('text', ['name' => 'to_zip', 'size' => 5]) ?>
        </td></tr>

    <tr><th>Package:</th><td></td></tr>
    <tr><td>Weight:</td>
        <td><?= $form->input('text', ['name' => 'weight']) ?></td></tr>
    <tr><td>Height:</td>
        <td><?= $form->input('text', ['name' => 'height']) ?></td></tr>
    <tr><td>Width:</td>
        <td><?= $form->input('text', ['name' => 'width']) ?></td></tr>
    <tr><td>Depth:</td>
        <td><?= $form->input('text', ['name' => 'depth']) ?></td></tr>

    <tr><td colspan="2" align="center">
    <?= $form->input('submit', ['value' => 'Ship!']) ?>
    </td></tr>

</table>
</form>

Exercise 5

function print_array($ar) {
    print '<ul>';
    foreach ($ar as $k => $v) {
        if (is_array($v)) {
            print '<li>' . htmlentities($k) .':</li>';
            print_array($v);
        } else {
            print '<li>' . htmlentities($k) .'=' . htmlentities($v) . '</li>';
        }
    }
    print '</ul>';
}

/* Since this is operating on form data, it looks directly at $_POST
   instead of a validated $input array */
function process_form() {
    print_array($_POST);
}

Chapter 8

Exercise 1

try {
    // Connect
    $db = new PDO('sqlite:/tmp/restaurant.db');
    // Set up exceptions on DB errors
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $stmt = $db->query('SELECT * FROM dishes ORDER BY price');
    $dishes = $stmt->fetchAll();
    if (count($dishes) == 0) {
        $html = '<p>No dishes to display</p>';
    } else {
        $html = "<table>\n";
        $html .= "<tr><th>Dish Name</th><th>Price</th><th>Spicy?</th></tr>\n";
        foreach ($dishes as $dish) {
            $html .= '<tr><td>' .
                  htmlentities($dish['dish_name']) . '</td><td>$' .
                  sprintf('%.02f', $dish['price']) . '</td><td>' .
                  ($dish['is_spicy'] ? 'Yes' : 'No') . "</td></tr>\n";
        }
        $html .= "</table>";
    }
} catch (PDOException $e) {
    $html = "Can't show dishes: " . $e->getMessage();
}
print $html;

Exercise 2

<?php

// Load the form helper class
require 'FormHelper.php';

// Connect to the database
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (PDOException $e) {
    print "Can't connect: " . $e->getMessage();
    exit();
}
// Set up exceptions on DB errors
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Set up fetch mode: rows as objects
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);

// 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()) {
    // Set up the $form object with proper defaults
    $form = new FormHelper();
    // All the HTML and form display is in a separate file for clarity
    include 'price-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    // Minimum price must be a valid floating-point number
    $input['min_price'] = filter_input(INPUT_POST,'min_price', 
    FILTER_VALIDATE_FLOAT);
    if ($input['min_price'] === null || $input['min_price'] === false) {
        $errors[] = 'Please enter a valid minimum price.';
    }
    return array($errors, $input);
}

function process_form($input) {
    // Access the global variable $db inside this function
    global $db;

    // Build up the query
    $sql = 'SELECT dish_name, price, is_spicy FROM dishes WHERE
            price >= ?';

    // Send the query to the database program and get all the rows back
    $stmt = $db->prepare($sql);
    $stmt->execute(array($input['min_price']));
    $dishes = $stmt->fetchAll();

    if (count($dishes) == 0) {
        print 'No dishes matched.';
    } else {
        print '<table>';
        print '<tr><th>Dish Name</th><th>Price</th><th>Spicy?</th></tr>';
        foreach ($dishes as $dish) {
            if ($dish->is_spicy == 1) {
                $spicy = 'Yes';
            } else {
                $spicy = 'No';
            }
            printf('<tr><td>%s</td><td>$%.02f</td><td>%s</td></tr>',
                   htmlentities($dish->dish_name), $dish->price, $spicy);
        }
        print '</table>';
    }
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The price-form.php file referenced, which displays the form HTML, contains:

<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>Minimum Price:</td>
        <td><?= $form->input('text',['name' => 'min_price']) ?></td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <?= $form->input('submit', ['name' => 'search',
                                        'value' => 'Search']) ?></td>
    </tr>
</table>
</form>

Exercise 3

<?php

// Load the form helper class
require 'FormHelper.php';

// Connect to the database
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (PDOException $e) {
    print "Can't connect: " . $e->getMessage();
    exit();
}
// Set up exceptions on DB errors
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Set up fetch mode: rows as objects
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);

// 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()) {
    global $db;

    // Set up the $form object with proper defaults
    $form = new FormHelper();

    // Retrieve the list of dish names to use from the database
    $sql = 'SELECT dish_id, dish_name FROM dishes ORDER BY dish_name';
    $stmt = $db->query($sql);
    $dishes = array();
    while ($row = $stmt->fetch()) {
        $dishes[$row->dish_id] = $row->dish_name;
    }

    // All the HTML and form display is in a separate file for clarity
    include 'dish-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    // As long as some dish_id value is submitted, we'll consider it OK.
    // If it doesn't match any dishes in the database, process_form()
    // can report that.
    if (isset($_POST['dish_id'])) {
        $input['dish_id'] = $_POST['dish_id'];
    } else {
        $errors[] = 'Please select a dish.';
    }
    return array($errors, $input);
}

function process_form($input) {
    // Access the global variable $db inside this function
    global $db;

    // Build up the query
    $sql = 'SELECT dish_id, dish_name, price, is_spicy FROM dishes WHERE
            dish_id = ?';

    // Send the query to the database program and get all the rows back
    $stmt = $db->prepare($sql);
    $stmt->execute(array($input['dish_id']));
    $dish = $stmt->fetch();

    if (count($dish) == 0) {
        print 'No dishes matched.';
    } else {
        print '<table>';
        print '<tr><th>ID</th><th>Dish Name</th><th>Price</th>';
        print '<th>Spicy?</th></tr>';
        if ($dish->is_spicy == 1) {
            $spicy = 'Yes';
        } else {
            $spicy = 'No';
        }
        printf('<tr><td>%d</td><td>%s</td><td>$%.02f</td><td>%s</td></tr>',
               $dish->dish_id,
               htmlentities($dish->dish_name), $dish->price, $spicy);
        print '</table>';
    }
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The dish-form.php file referenced, which displays the form HTML, contains:

<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>Dish:</td>
        <td><?= $form->select($dishes,['name' => 'dish_id']) ?></td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <?= $form->input('submit', ['name' => 'info',
                                        'value' => 'Get Dish Info']) ?></td>
    </tr>
</table>
</form>

Exercise 4

<?php

// Load the form helper class
require 'FormHelper.php';

// Connect to the database
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (PDOException $e) {
    print "Can't connect: " . $e->getMessage();
    exit();
}
// Set up exceptions on DB errors
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Set up fetch mode: rows as objects
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);

// Put the list of dish IDs and names in a global array because
// we'll need it in show_form() and validate_form()
$dishes = array();
$sql = 'SELECT dish_id, dish_name FROM dishes ORDER BY dish_name';
$stmt = $db->query($sql);
while ($row = $stmt->fetch()) {
    $dishes[$row->dish_id] = $row->dish_name;
}

// 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()) {
    global $db, $dishes;

    // Set up the $form object with proper defaults
    $form = new FormHelper();

    // All the HTML and form display is in a separate file for clarity
    include 'customer-form.php';
}

function validate_form() {
    global $dishes;
    $input = array();
    $errors = array();

    // Make sure a dish_id valid is submitted and in $dishes.
    // As long as some dish_id value is submitted, we'll consider it OK.
    // If it doesn't match any dishes in the database, process_form()
    // can report that.
    $input['dish_id'] = $_POST['dish_id'] ?? '';
    if (! array_key_exists($input['dish_id'], $dishes)) {
        $errors[] = 'Please select a valid dish.';
    }

    // Name is required
    $input['name'] = trim($_POST['name'] ?? '');
    if (0 == strlen($input['name'])) {
        $errors[] = 'Please enter a name.';
    }

    // Phone number is required
    $input['phone'] = trim($_POST['phone'] ?? '');
    if (0 == strlen($input['phone'])) {
        $errors[] = 'Please enter a phone number.';
    } else {
        // Be US-centric and ensure that the phone number contains
        // at least 10 digits. Using ctype_digit() on each
        // character is not the most efficient way to do this,
        // but is logically straightforward and avoids
        // regular expressions.
        $digits = 0;
        for ($i = 0; $i < strlen($input['phone']); $i++) {
            if (ctype_digit($input['phone'][$i])) {
                $digits++;
            }
        }
        if ($digits < 10) {
            $errors[] = 'Phone number needs at least ten digits.';
        }
    }

    return array($errors, $input);
}

function process_form($input) {
    // Access the global variable $db inside this function
    global $db;

    // Build up the query. No need to specify customer_id because
    // the database will automatically assign a unique one.
    $sql = 'INSERT INTO customers (name,phone,favorite_dish_id) ' .
           'VALUES (?,?,?)';

    // Send the query to the database program and get all the rows back
    try {
        $stmt = $db->prepare($sql);
        $stmt->execute(array($input['name'],$input['phone'],$input['dish_id']));
        print '<p>Inserted new customer.</p>';
    } catch (Exception $e) {
        print "<p>Couldn't insert customer: {$e->getMessage()}.</p>";
    }
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The customer-form.php file referenced, which displays the form HTML, contains:

<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>
    <tr><td>Name:</td><td><?= $form->input('text', ['name' => 'name']) ?>
    </td></tr>
    <tr><td>Phone Number:</td>
        <td><?= $form->input('text', ['name' => 'phone']) ?></td></tr>
    <tr><td>Favorite Dish:</td>
        <td><?= $form->select($dishes,['name' => 'dish_id']) ?></td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <?= $form->input('submit', ['name' => 'add',
                                        'value' => 'Add Customer']) ?></td>
    </tr>
</table>
</form>

Chapter 9

Exercise 1

The template file, template.html:

<html>
    <head><title>{title}</title></head>
    <body>
        <h1>{headline}</h1>
        <h2>By {byline}</h2>
        <div class="article">{article}</div>
        <p><small>Page generated: {date}</small></p>
    </body>
</html>

The PHP program to replace template variables:

$now = new DateTime();
// Express the vars as simply as possible, just key => value
$vars = array('title' => 'Man Bites Dog',
              'headline' => 'Man and Dog Trapped in Biting Fiasco',
              'byline' => 'Ireneo Funes',
              'article' => <<<_HTML_
<p>While walking in the park today, Bioy Casares took a big juicy
bite out of his dog, Santa's Little Helper. When asked why he did
it, Mr. Casares said, "I was hungry."</p>
_HTML_
              ,
              'date' => $now->format('l, F j, Y'));

// Make a version of $vars to match the templating syntax, with
// {} around the keys
$template_vars = array();
foreach ($vars as $k => $v) {
    $template_vars['{'.$k.'}'] = $v;
}
// Load the template
$template = file_get_contents('template.html');
if ($template === false) {
    die("Can't read template.html: $php_errormsg");
}
// If given an array of strings to look for and an array of replacements,
// str_replace() does all the replacements at once for you
$html = str_replace(array_keys($template_vars),
                    array_values($template_vars),
                    $template);
// Write out the new HTML page
$result = file_put_contents('article.html', $html);
if ($result === false) {
    die("Can't write article.html: $php_errormsg");
}

Exercise 2

// The array to accumulate address counts
$addresses = array();

$fh = fopen('addresses.txt','rb');
if (! $fh) {
    die("Can't open addresses.txt: $php_errormsg");
}
while ((! feof($fh)) && ($line = fgets($fh))) {
    $line = trim($line);
    // Use the address as the key in $addresses. The value is the number
    // of times the address has appeared.
    if (! isset($addresses[$line])) {
        $addresses[$line] = 0;
    }
    $addresses[$line] = $addresses[$line] + 1;
}
if (! fclose($fh)) {
    die("Can't close addresses.txt: $php_errormsg");
}

// Reverse sort (biggest first) $addresses by element value
arsort($addresses);

$fh = fopen('addresses-count.txt','wb');
if (! $fh) {
    die("Can't open addresses-count.txt: $php_errormsg");
}
foreach ($addresses as $address => $count) {
    // Don't forget the newline at the end
    if (fwrite($fh, "$count,$address\n") === false) {
        die("Can't write $count,$address: $php_errormsg");
    }
}
if (! fclose($fh)) {
    die("Can't close addresses-count.txt: $php_errormsg");
}

Here is a sample addresses.txt to use:

brilling@tweedledee.example.com
slithy@unicorn.example.com
uffish@knight.example.net
slithy@unicorn.example.com
jubjub@sheep.example.com
tumtum@queen.example.org
slithy@unicorn.example.com
uffish@knight.example.net
manxome@king.example.net
beamish@lion.example.org
uffish@knight.example.net
frumious@tweedledum.example.com
tulgey@carpenter.example.com
vorpal@crow.example.org
beamish@lion.example.org
mimsy@walrus.example.com
frumious@tweedledum.example.com
raths@owl.example.net
frumious@tweedledum.example.com

Exercise 3

$fh = fopen('dishes.csv','rb');
if (! $fh) {
    die("Can't open dishes.csv: $php_errormsg");
}
print "<table>\n";
while ((! feof($fh)) && ($line = fgetcsv($fh))) {
    // Using implode() as in Chapter 4
    print "<tr><td>" . implode("</td><td>", $line) . "</td></tr>\n";
}
print "</table>";

Exercise 4

<?php

// Load the form helper class
require 'FormHelper.php';

// 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()) {
    // Set up the $form object with proper defaults
    $form = new FormHelper();

    // All the HTML and form display is in a separate file for clarity
    include 'filename-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    // Make sure a filename is specified
    $input['file'] = trim($_POST['file'] ?? '');
    if (0 == strlen($input['file'])) {
        $errors[] = 'Please enter a filename.';
    } else {
        // Make sure the full filename is under the web
        // server's document root
        $full = $_SERVER['DOCUMENT_ROOT'] . '/' . $input['file'];
        // Use realpath() to resolve any .. sequences or
        // symbolic links
        $full = realpath($full);
        if ($full === false) {
            $errors[] = "Please enter a valid filename.";
        } else {
            // Make sure $full begins with the document root directory
            $docroot_len = strlen($_SERVER['DOCUMENT_ROOT']);
            if (substr($full, 0, $docroot_len) != $_SERVER['DOCUMENT_ROOT']) {
                $errors[] = 'File must be under document root.';
            } else {
                // If it's OK, put the full path in $input so we can use
                // it in process_form()
                $input['full'] = $full;
            }
        }
    }

    return array($errors, $input);
}

    function process_form($input) {
    if (is_readable($input['full'])) {
        print htmlentities(file_get_contents($input['full']));
    } else {
        print "Can't read {$input['file']}.";
    }
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The filename-form.php file referenced, which displays the form HTML, contains:

<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>File:</td>
        <td><?= $form->input('text', ['name' => 'file']) ?></td></tr>
    <tr><td colspan="2" 
         align="center"><?= $form->input('submit', ['value' => 'Display']) ?>
    </td></tr>

</table>
</form>

Exercise 5

Here is the new validate_form() function that implements the additional test using strcasecmp():

function validate_form() {
    $input = array();
    $errors = array();

    // Make sure a filename is specified
    $input['file'] = trim($_POST['file'] ?? '');
    if (0 == strlen($input['file'])) {
        $errors[] = 'Please enter a filename.';
    } else {
        // Make sure the full filename is under the web
        // server's document root
        $full = $_SERVER['DOCUMENT_ROOT'] . '/' . $input['file'];
        // Use realpath() to resolve any .. sequences or
        // symbolic links
        $full = realpath($full);
        if ($full === false) {
            $errors[] = "Please enter a valid filename.";
        } else {
            // Make sure $full begins with the document root directory
            $docroot_len = strlen($_SERVER['DOCUMENT_ROOT']);
            if (substr($full, 0, $docroot_len) != $_SERVER['DOCUMENT_ROOT']) {
                $errors[] = 'File must be under document root.';
            } else if (strcasecmp(substr($full, -5), '.html') != 0) {
                $errors[] = 'File name must end in .html';
            } else {
                // If it's OK, put the full path in $input so we can use
                // it in process_form()
                $input['full'] = $full;
            }
        }
    }

    return array($errors, $input);
}

Chapter 10

Exercise 1

$view_count = 1 + ($_COOKIE['view_count'] ?? 0);
setcookie('view_count', $view_count);
print "<p>Hi! Number of times you've viewed this page: $view_count.</p>";

Exercise 2

$view_count = 1 + ($_COOKIE['view_count'] ?? 0);

if ($view_count == 20) {
    // An empty value for setcookie() removes the cookie
    setcookie('view_count', '');
    $msg = "<p>Time to start over.</p>";
} else {
    setcookie('view_count', $view_count);
    $msg = "<p>Hi! Number of times you've viewed this page: $view_count.</p>";
    if ($view_count == 5) {
        $msg .= "<p>This is your fifth visit.</p>";
    } elseif ($view_count == 10) {
        $msg .= "<p>This is your tenth visit. You must like this page.</p>";
    } elseif ($view_count == 15) {
        $msg .= "<p>This is your fifteenth visit. " .
                "Don't you have anything else to do?</p>";
    }
}
print $msg;

Exercise 3

The color-picking page:

<?php
// Start sessions first thing so we can use $_SESSION freely later
session_start();

// Load the form helper class
require 'FormHelper.php';

$colors = array('ff0000' => 'Red',
                'ffa500' => 'Orange',
                'ffffff' => 'Yellow',
                '008000' => 'Green',
                '0000ff' => 'Blue',
                '4b0082' => 'Indigo',
                '663399' => 'Rebecca Purple');

// 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()) {
    global $colors;

    // Set up the $form object with proper defaults
    $form = new FormHelper();
    // All the HTML and form display is in a separate file for clarity
    include 'color-form.php';
}

function validate_form() {
    $input = array();
    $errors = array();

    // color must be a valid color
    $input['color'] = $_POST['color'] ?? '';
    if (! array_key_exists($input['color'], $GLOBALS['colors'])) {
        $errors[] = 'Please select a valid color.';
    }

    return array($errors, $input);
}

function process_form($input) {
    global $colors;

    $_SESSION['background_color'] = $input['color'];
    print '<p>Your color has been set.</p>';
}
?>

The code relies on the FormHelper.php file discussed in Chapter 7. The color-form.php file referenced, which displays the form HTML, contains:

<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>Favorite Color:</td>
        <td><?= $form->select($colors,['name' => 'color']) ?></td>
    </tr>
    <tr>
        <td colspan="2" align="center">
            <?= $form->input('submit', ['name' => 'set',
                                        'value' => 'Set Color']) ?></td>
    </tr>
</table>
</form>

The page with background color set:

<?php
// Start sessions first thing so we can use $_SESSION freely later
session_start();
?>
<html>
  <head><title>Background Color Example</title>
  <body style="background-color:<?= $_SESSION['background_color'] ?>">
    <p>What color did you pick?</p>
  </body>
</html>

Exercise 4

The ordering page:

session_start();

// This assumes FormHelper.php is in the same directory as
// this file.
require 'FormHelper.php';

// Set up the array of choices in the select menu.
// This is needed in display_form(), validate_form(),
// and process_form(), so it is declared in the global scope.
$products = [ '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()) {
    global $products;
    $defaults = array();
    // Start out with 0 as a default
    foreach ($products as $code => $label) {
        $defaults["quantity_$code"] = 0;
    }
    // If quantities are in the session, use those
    if (isset($_SESSION['quantities'])) {
        foreach ($_SESSION['quantities'] as $field => $quantity) {
            $defaults[$field] = $quantity;
        }
    }
    $form = new FormHelper($defaults);
    // All the HTML and form display is in a separate file for clarity
    include 'order-form.php';
}

function validate_form() {
    global $products;

    $input = array();
    $errors = array();

    // For each quantity box, make sure the value is
    // a valid integer >= 0
    foreach ($products as $code => $name) {
        $field = "quantity_$code";
        $input[$field] = filter_input(INPUT_POST, $field,
                                      FILTER_VALIDATE_INT,
                                      ['options' => ['min_range'=>0]]);
        if (is_null($input[$field]) || ($input[$field] === false)) {
            $errors[] = "Please enter a valid quantity for $name.";
        }
    }

    return array($errors, $input);
}

function process_form($input) {
    $_SESSION['quantities'] = $input;

    print "Thank you for your order.";
}

The code relies on the FormHelper.php file discussed in Chapter 7. The order-form.php file referenced, which displays the form HTML, contains:

<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><th>Product</th><td>Quantity</td></tr>
<?php foreach ($products as $code => $name) { ?>
    <tr><td><?= htmlentities($name) ?>:</td>
        <td><?= $form->input('text', ['name' => "quantity_$code"]) ?></td></tr>
<?php } ?>
    <tr><td colspan="2" 
         align="center"><?= $form->input('submit', ['value' => 'Order']) ?>
    </td></tr>

</table>
</form>

The checkout page:

session_start();

// The same products from the order page
$products = ['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'];

// Simplified main page logic without form validation
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    process_form();
} else {
    // The form wasn't submitted, so display
    show_form();
}

function show_form() {
    global $products;

    // The "form" is just a single submit button, so we won't use
    // FormHelper and just inline all the HTML here
    if (isset($_SESSION['quantities']) && (count($_SESSION['quantities'])>0)) {
        print "<p>Your order:</p><ul>";
        foreach ($_SESSION['quantities'] as $field => $amount) {
            list($junk, $code) = explode('_', $field);
            $product = $products[$code];
            print "<li>$amount $product</li>";
        }
        print "</ul>";
        print '<form method="POST" action=' .
              htmlentities($_SERVER['PHP_SELF']) . '>';
        print '<input type="submit" value="Check Out" />';
        print '</form>';
    } else {
        print "<p>You don't have a saved order.</p>";
    }
    // This assumes the order form page is saved as "order.php"
    print '<a href="order.php">Return to Order page</a>';
}

function process_form() {
    // This removes the data from the session
    unset($_SESSION['quantities']);
    print "<p>Thanks for your order.</p>";
}

Chapter 11

Exercise 1

$json = file_get_contents("http://php.net/releases/?json");
if ($json === false) {
    print "Can't retrieve feed.";
}
else {
    $feed = json_decode($json, true);
    // $feed is an array whose top-level keys are major release
    // numbers. First we need to pick the biggest one.
    $major_numbers = array_keys($feed);
    rsort($major_numbers);
    $biggest_major_number = $major_numbers[0];
    // The "version" element in the array under the major number
    // key is the latest release for that major version number
    $version = $feed[$biggest_major_number]['version'];
    print "The latest version of PHP released is $version.";
}

Exercise 2

$c = curl_init("http://php.net/releases/?json");
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
$json = curl_exec($c);
if ($json === false) {
    print "Can't retrieve feed.";
}
else {
    $feed = json_decode($json, true);
    // $feed is an array whose top-level keys are major release
    // numbers. First we need to pick the biggest one.
    $major_numbers = array_keys($feed);
    rsort($major_numbers);
    $biggest_major_number = $major_numbers[0];
    // The "version" element in the array under the major number
    // key is the latest release for that major version number
    $version = $feed[$biggest_major_number]['version'];
    print "The latest version of PHP released is $version.";
}

Exercise 3

// Seconds from Jan 1, 1970 until now
$now = time();
setcookie('last_access', $now);
if (isset($_COOKIE['last_access'])) {
    // To create a DateTime from a seconds-since-1970 value,
    // prefix it with @.
    $d = new DateTime('@'. $_COOKIE['last_access']);
    $msg = '<p>You last visited this page at ' .
         $d->format('g:i a') . ' on ' .
         $d->format('F j, Y') . '</p>';
} else {
    $msg = '<p>This is your first visit to this page.</p>';
}

print $msg;

Exercise 4

$url = 'https://api.github.com/gists';
$data = ['public' => true,
         'description' => "This program a gist of itself.",
         // As the API docs say:
         // The keys in the files object are the string filename,
         // and the value is another object with a key of content
         // and a value of the file contents.
         'files' => [ basename(__FILE__) =>
                      [ 'content' => file_get_contents(__FILE__) ] ] ];

$c = curl_init($url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
curl_setopt($c, CURLOPT_POST, true);
curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($c, CURLOPT_USERAGENT, 'learning-php-7/exercise');

$response = curl_exec($c);
if ($response === false) {
    print "Couldn't make request.";
} else {
    $info = curl_getinfo($c);
    if ($info['http_code'] != 201) {
        print "Couldn't create gist, got {$info['http_code']}\n";
        print $response;
    } else {
        $body = json_decode($response);
        print "Created gist at {$body->html_url}\n";
    }
}

Chapter 12

Exercise 1

The keyword global should not be in line 5, so the parse error should report that unexpected keyword. The actual parse error is:

PHP Parse error:  syntax error, unexpected 'global' (T_GLOBAL)
in debugging-12.php on line 5

To make the program run properly, change the line print global $name; to print $GLOBALS['name'];. Or, you can add global name; as the first line of the function and then change print global $name; to print $name;.

Exercise 2

function validate_form() {
    $input = array();
    $errors = array();

    // turn on output buffering
    ob_start();
    // dump all the submitted data
    var_dump($_POST);
    // capture the generated "output"
    $output = ob_get_contents();
    // turn off output buffering
    ob_end_clean();
    // send the variable dump to the error log
    error_log($output);

    // op is required
    $input['op'] = $GLOBALS['ops'][$_POST['op']] ?? '';
    if (! in_array($input['op'], $GLOBALS['ops'])) {
        $errors[] = 'Please select a valid operation.';
    }
    // num1 and num2 must be numbers
    $input['num1'] = filter_input(INPUT_POST, 'num1', FILTER_VALIDATE_FLOAT);
    if (is_null($input['num1']) || ($input['num1'] === false)) {
        $errors[] = 'Please enter a valid first number.';
    }

    $input['num2'] = filter_input(INPUT_POST, 'num2', FILTER_VALIDATE_FLOAT);
    if (is_null($input['num2']) || ($input['num2'] === false)) {
        $errors[] = 'Please enter a valid second number.';
    }

    // can't divide by zero
    if (($input['op'] == '/') && ($input['num2'] == 0)) {
        $errors[] = 'Division by zero is not allowed.';
    }

    return array($errors, $input);
}

Exercise 3

At the top of the program, this code defines an exception handler and sets it up to be called on unhandled exceptions:

function exceptionHandler($ex) {
    // Log the specifics to the error log
    error_log("ERROR: " . $ex->getMessage());
    // Print something less specific for users to see
    // and exit
    die("<p>Sorry, something went wrong.</p>");
}
set_exception_handler('exceptionHandler');

Then the try/catch blocks can be removed from the two places they are used (once around creating the PDO object and once in process_form()) because the exceptions will be handled by the exception handler.

Exercise 4

  • Line 4: Change :: to : in the DSN.
  • Line 5: Change catch ($e) to catch (Exception $e).
  • Line 16: Change $row['dish_id']] to $row['dish_id'] as the key to look up in the $dish_names array.
  • Line 18: Change ** to * in the SQL query.
  • Line 20: Change = to ==.
  • Line 26: Change the third format specifier from %f to %s$customer['phone'] is a string.
  • Line 30: Change $customer['favorite_dish_id'] to $di⁠sh_⁠na⁠mes​[$cus⁠tomer['favo⁠rite_dish_id']] so that the dish ID is translated into the name of the corresponding dish.
  • Line 33: Insert a } to match the opening { in line 22.

The complete corrected program is:

<?php
// Connect to the database
try {
    $db = new PDO('sqlite:/tmp/restaurant.db');
} catch (Exception $e) {
    die("Can't connect: " . $e->getMessage());
}
// Set up exception error handling
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Set up fetch mode: rows as arrays
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Get the array of dish names from the database
$dish_names = array();
$res = $db->query('SELECT dish_id,dish_name FROM dishes');
foreach ($res->fetchAll() as $row) {
    $dish_names[ $row['dish_id'] ] = $row['dish_name'];
}
$res = $db->query('SELECT * FROM customers ORDER BY phone DESC');
$customers = $res->fetchAll();
if (count($customers) == 0) {
    print "No customers.";
} else {
    print '<table>';
    print '<tr><th>ID</th><th>Name</th>
           <th>Phone</th><th>Favorite Dish</th></tr>';
    foreach ($customers as $customer) {
        printf("<tr><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>\n",
               $customer['customer_id'],
               htmlentities($customer['customer_name']),
               $customer['phone'],
               $dish_names[$customer['favorite_dish_id']]);
    }
    print '</table>';
}
?>

Chapter 13

Exercise 2

public function testNameMustBeSubmitted() {
        $submitted = array('age' => '15',
                           'price' => '39.95');
        list($errors, $input) = validate_form($submitted);
        $this->assertContains('Your name is required.', $errors);
        $this->assertCount(1, $errors);

    }

Exercise 3

include 'FormHelper.php';

class FormHelperTest extends PHPUnit_Framework_TestCase {

    public $products = [ 'cu&ke'    => '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'];
    public $stooges = ['Larry','Moe','Curly','Shemp'];

    // This code gets run before each test. Putting it in
    // the special setUp() method is more concise than having
    // to repeat it in each test method.
    public function setUp() {
        $_SERVER['REQUEST_METHOD'] = 'GET';
    }

    public function testAssociativeOptions() {
        $form = new FormHelper();
        $html = $form->select($this->products);
        $this->assertEquals($html,<<<_HTML_
<select ><option  value="cu&amp;ke">Braised &lt;Sea&gt; Cucumber</option>
<option  value="stomach">Sauteed Pig's Stomach</option>
<option  value="tripe">Sauteed Tripe with Wine Sauce</option>
<option  value="taro">Stewed Pork with Taro</option>
<option  value="giblets">Baked Giblets with Salt</option>
<option  value="abalone">Abalone with Marrow and Duck Feet</option></select>
_HTML_
        );
    }

    public function testNumericOptions() {
        $form = new FormHelper();
        $html = $form->select($this->stooges);
        $this->assertEquals($html,<<<_HTML_
<select ><option  value="0">Larry</option>
<option  value="1">Moe</option>
<option  value="2">Curly</option>
<option  value="3">Shemp</option></select>
_HTML_
        );
    }

    public function testNoOptions() {
        $form = new FormHelper();
        $html = $form->select([]);
        $this->assertEquals('<select ></select>', $html);
    }

    public function testBooleanTrueAttributes() {
        $form = new FormHelper();
        $html = $form->select([],['np' => true]);
        $this->assertEquals('<select np></select>', $html);

    }

    public function testBooleanFalseAttributes() {
        $form = new FormHelper();
        $html = $form->select([],['np' => false, 'onion' => 'red']);
        $this->assertEquals('<select onion="red"></select>', $html);
    }

    public function testNonBooleanAttributes() {
        $form = new FormHelper();
        $html = $form->select([],['spaceship'=>'<=>']);
        $this->assertEquals('<select spaceship="&lt;=&gt;"></select>', $html);
    }

    public function testMultipleAttribute() {
        $form = new FormHelper();
        $html = $form->select([],["name" => "menu",
                                  "q" => 1, "multiple" => true]);
        $this->assertEquals('<select name="menu[]" q="1" multiple></select>', 
        $html);
    }
}

Exercise 4

The additional test methods for FormHelperTest:

public function testButtonNoTypeOK() {
        $form = new FormHelper();
        $html = $form->tag('button');
        $this->assertEquals('<button  />',$html);
    }
    public function testButtonTypeSubmitOK() {
        $form = new FormHelper();
        $html = $form->tag('button',['type'=>'submit']);
        $this->assertEquals('<button type="submit" />',$html);
    }
    public function testButtonTypeResetOK() {
        $form = new FormHelper();
        $html = $form->tag('button',['type'=>'reset']);
        $this->assertEquals('<button type="reset" />',$html);
    }
    public function testButtonTypeButtonOK() {
        $form = new FormHelper();
        $html = $form->tag('button',['type'=>'button']);
        $this->assertEquals('<button type="button" />',$html);
    }
    public function testButtonTypeOtherFails() {
        $form = new FormHelper();
        // FormHelper should throw an InvalidArgumentException
        // when an invalid attribute is provided
        $this->setExpectedException('InvalidArgumentException');
        $html = $form->tag('button',['type'=>'other']);
    }

The necessary modifications for FormHelper that make the tests pass are:

// This code goes just after the "class FormHelper" declaration
    // This array expresses, for the specified elements,
    // what attribute names have what allowed values
    protected $allowedAttributes = ['button' => ['type' => ['submit',
                                                            'reset',
                                                            'button' ] ] ];


    // tag() is modified to pass $tag as the first argument to
    // $this->attributes()
    public function tag($tag, $attributes = array(), $isMultiple = false) {
        return "<$tag {$this->attributes($tag, $attributes, $isMultiple)} />";
    }

    // start() is also modified to pass $tag as the first argument to
    // $this->attributes()
    public function start($tag, $attributes = array(), $isMultiple = false) {
        // <select> and <textarea> tags don't get value attributes on them
        $valueAttribute = (! (($tag == 'select')||($tag == 'textarea')));
        $attrs = $this->attributes($tag, $attributes, $isMultiple,
                                   $valueAttribute);
        return "<$tag $attrs>";
    }


    // attributes() is modified to accept $tag as a first argument,
    // set up $attributeCheck if allowed attributes for the tag have
    // been defined in $this->allowedAttributes, and then, if allowed
    // attributes have been defined, see if the provided value is
    // allowed and throw an exception if not
    protected function attributes($tag, $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']];
        }
        if (isset($this->allowedAttributes[$tag])) {
            $attributeCheck = $this->allowedAttributes[$tag];
        } else {
            $attributeCheck = array();
        }
        foreach ($attributes as $k => $v) {
            // Check if the attribute's value is allowed
            if (isset($attributeCheck[$k]) &&
                (! in_array($v, $attributeCheck[$k]))) {
                throw new 
                InvalidArgumentException("$v is not allowed as value for $k");
            }
            // 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);
    }