Complete validation

See more about:

Thanks to Miles Thompson for his help with this lesson.

Where are we?

OK, let’s bring it all together.

  • We’ll use the nice error reporting we saw in the previous lesson.
  • We’ll use JavaScript to check for client-side errors (like non-numbers in numeric fields), and PHP to check for errors on the server side.
  • Both client-side and server-side errors will be reported in the same way.

What the user sees

Here’s how things start:

Empty form

Figure 1. Empty form

Client-side checking makes error messages like this:

Error messages

Figure 2. Error messages

What if the user wants to order more than we have on hand?

Too many

Figure 3. Too many

The error message in Figure 3 tells the user how many we have on hand.

Again, this may not be good business practice, but it’s good for learning about PHP.

Try the page. Try making various kinds of errors. You can also download the files.

The overall pattern

The overall processing pattern is the same as before:

Overall pattern

Figure 4. Overall pattern

The form page passes data to itself for validation. If everything is OK, the page passes the data to a processing page.

There are a few improvements over the client-side only version on the previous page.

  • Empty fields are handled better.
  • Check on-hand inventory.
  • Server-side errors (like ordering more than are on hand) are reported to the user with the client-side error reporting mechanism (the animated messages with the icon).

The complete order.php code

Here’s the complete code. We’ll talk about the changes in the next section.

<?php
if ( $_POST ) {
  //There is order data to process.
  require 'validation-library.inc';
  //Get the order amounts.
  $frisbees = $_POST['frisbees'];
  if ( $frisbees == '' ) {
    $frisbees = 0;
  }
  $giant_chew_ropes = $_POST['giant_chew_ropes'];
  if ( $giant_chew_ropes == '' ) {
    $giant_chew_ropes = 0;
  }
  $squeaky_balls = $_POST['squeaky_balls'];
  if ( !isset($squeaky_balls) || $squeaky_balls == '' ) {
    $squeaky_balls = 0;
  }

  //Flag for data OK.
  $data_ok = true;
  //JavaScript to run after page load.
  $js = '';

  //Check frisbees.
  $frisbees_message = check_number_ordered($frisbees);
  if ( $frisbees_message != '' ) {
    $data_ok = false;
    $js .= 'show_product_error_message("'.$frisbees_message . '", "frisbees");';
  }

  //Check giant chew ropes.
  $giant_chew_ropes_message = check_number_ordered($giant_chew_ropes);
  if ( $giant_chew_ropes_message != '' ) {
    $data_ok = false;
    $js .= 'show_product_error_message("'.$giant_chew_ropes_message . '", "giant_chew_ropes");';
  }

  //Check squeaky balls.
  $squeaky_balls_message = check_number_ordered($squeaky_balls);
  if ( $squeaky_balls_message != '' ) {
    $data_ok = false;
    $js .= 'show_product_error_message("'.$squeaky_balls_message . '", "squeaky_balls");';
  }

  //Check that at least one item was ordered.
  if ( $frisbees + $giant_chew_ropes + $squeaky_balls == 0 ) {
    $data_ok = false;
    $js .= 'show_product_global_error_message("Sorry, you must order at least one item.");';
  }

  //Check number on hand.
  //Normally number on hand would be loaded from a database.
  //For simplicity, I just made up some numbers.
  $frisbees_on_hand = 5;
  $giant_chew_ropes_on_hand = 4;
  $squeaky_balls_on_hand = 8;
  if ( $frisbees > $frisbees_on_hand ) {
    $data_ok = false;
    $js .= "show_product_error_message('Sorry, we only have $frisbees_on_hand on hand.', 'frisbees');";
  }
  if ( $giant_chew_ropes > $giant_chew_ropes_on_hand ) {
    $data_ok = false;
    $js .= "show_product_error_message('Sorry, we only have $giant_chew_ropes_on_hand on hand.', 'giant_chew_ropes');";
  }
  if ( $squeaky_balls > $squeaky_balls_on_hand ) {
    $data_ok = false;
    $js .= "show_product_error_message('Sorry, we only have $squeaky_balls_on_hand on hand.', 'squeaky_balls');";
  }

  //Is everything OK?
  if ( $data_ok ) {
    //Process the order.
    $destination_url = "process.php?frisbees=$frisbees&giant_chew_ropes=$giant_chew_ropes&squeaky_balls=$squeaky_balls";
    header("Location:" . $destination_url);
    exit();
  }
}
?><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Order Form</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style type="text/css">
      body {
        font-family: Verdana, sans-serif;
        font-size: 14px;
        background-color: #FFFFE0;
      }
      .message_container {
        display: none;
        font-weight: bold;
        color: red;
      }
    </style>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
    <script type="text/javascript">
      $(document).ready(function() {
        $("#global_error_message_container").hide();
        $("#frisbees").focus();
        <?php print $js; ?>
        $("#order_form").submit(function() {
          //Set flag showing everything is OK.
          var data_ok = true;

          //Check the frisbees.
          var frisbees = $("#frisbees").val();
          var frisbees_message = check_order_value(frisbees);
          if ( frisbees_message != '' ) {
            data_ok = false;
            show_product_error_message(frisbees_message, 'frisbees');
          }
          else {
            hide_error_message('frisbees');
          }

          //Check the giant chew ropes.
          var giant_chew_ropes = $("#giant_chew_ropes").val();
          var giant_chew_ropes_message = check_order_value(giant_chew_ropes);
          if ( giant_chew_ropes_message != '' ) {
            data_ok = false;
            show_product_error_message(giant_chew_ropes_message, 'giant_chew_ropes');
          }
          else {
            hide_error_message('giant_chew_ropes');
          }

          //Check the squeaky balls.
          var squeaky_balls = $("#squeaky_balls").val();
          var squeaky_balls_message = check_order_value(squeaky_balls);
          if ( squeaky_balls_message != '' ) {
            data_ok = false;
            show_product_error_message(squeaky_balls_message, 'squeaky_balls');
          }
          else {
            hide_error_message('squeaky_balls');
          }

          //Make sure something was ordered.
          if ( frisbees + giant_chew_ropes + squeaky_balls == 0 ) {
            show_global_error_message("Sorry, you must order at least one item.");
            data_ok = false;
          }
          return data_ok;
        });
      });

      //Check whether an order value is valid.
      //Input
      //  value_to_check: the value to check.
      //Return: An error message, or empty string if no error.
      function check_order_value(value_to_check) {
        if ( isNaN(value_to_check) ) {
          return 'Please enter a number';
        }
        if ( value_to_check != Math.round(value_to_check) ){
          return 'Please enter whole numbers only';
        }
        if ( value_to_check < 0 ){
          return 'Please enter positive numbers only';
        }
        return '';
      }

      //Show an error message for a product.
      //Input
      //  message: The message to show.
      //  product_name: The product name the message is about.
      //Return: nothing.
      function show_product_error_message(message, product_name) {
        //Show the error message in the product's message area.
        $('#' + product_name + '_message').text(message);
        if ( $('#' + product_name + '_message_container').is(':hidden') ) {
          $('#' + product_name + '_message_container').show('medium');
        }
        //Show the global error message at the top of the page.
        show_global_error_message("Sorry, I can't process this order.");
      }

      //Show a global error message. It applies to the entire
      //  page, not just one product.
      //Input
      //  message: The message to show.
      //Return: nothing.
      function show_global_error_message(message) {
        $("#global_error_message").text(message);
        if ($('#global_error_message_container' ).is(':hidden') ) {
          $('#global_error_message_container').show('medium');
        }
      }

      //Hide a product error message.
      //Input
      //  product_name: The product name the message is about.
      //Return: nothing.
      function hide_error_message(product_name) {
        $('#' + product_name + '_message_container').hide('medium');
      }

    </script>
  </head>
  <body>
    <h1>Order Form</h1>
    <p>What would you like?</p>
    <p id="global_error_message_container" class="message_container">
      <img src="error.png" alt="Error">
      <span id="global_error_message"/>
    </p>
    <form id="order_form" method="post" action="order.php">
      <p>
        <input type="text" name="frisbees" id="frisbees" size="3"
        <?php
        if ( isset($_POST['frisbees']) ) {
          print ' value="' . $_POST['frisbees'] . '"';
        }
        ?>
        >
        Frisbees ($8.95 each)<br>
        <span id="frisbees_message_container" class="message_container">
          <img src="error.png" alt="Error">
          <span id="frisbees_message"/>
        </span>
      </p>
      <p>
        <input type="text" name="giant_chew_ropes" id="giant_chew_ropes" size="3"
        <?php
        if ( isset($_POST['giant_chew_ropes']) ) {
          print ' value="' . $_POST['giant_chew_ropes'] . '"';
        }
        ?>
        >
        Giant chew ropes ($12.95 each)<br>
        <span id="giant_chew_ropes_message_container" class="message_container">
          <img src="error.png" alt="Error">
          <span id="giant_chew_ropes_message"/>
        </span>
      </p>
      <p>
        <input type="text" name="squeaky_balls" id="squeaky_balls" size="3"
        <?php
        if ( isset($_POST['squeaky_balls']) ) {
          print ' value="' . $_POST['squeaky_balls'] . '"';
        }
        ?>
        >
        Squeaky balls to chase ($1.95 each)<br>
        <span id="squeaky_balls_message_container" class="message_container">
          <img src="error.png" alt="Error">
          <span id="squeaky_balls_message"/>
        </span>
      </p>
      <p>
        <button type="submit">Order</button>
      </p>
    </form>
  </body>
</html>

Figure 5. Complete order.php

Code changes

We’ve seen most of this before. But there are some changes.

Blank is zero

When a field is empty, the program decides the user wants none of that product:

//Get the order amounts.
$frisbees = $_POST['frisbees'];
if ( $frisbees == '' ) {
  $frisbees = 0;
}

Figure 6. When a field is empty

Form submission

On the previous page, where we did client-side only, we didn’t have a <form> tag. We attached validation code to a button:

$(document).ready(function() {
  ...
  $("#order_button").click(function() {
    ...
  });
});
...
<button id="order_button" type="button">Order</button>

Figure 7. Validation without a form

This time, we do have a <form>. We don’t attach the validation code to the click() event of a <button>. We attach it to the submit() event of the <form>.

$(document).ready(function() {
  ...
  $("#order_form").submit(function() {
    ...
  });
});
...
<form id="order_form" method="post" action="order.php">
  ...
  <button type="submit">Order</button>
  ...
</form>

Figure 8. Triggering validation with a form

We need a way to stop data from being submitted if it is invalid. We do this by returning either true or false from the submit() function.

  • If the submit() function returns true, the browser goes ahead and submits the data to the page given in the action property (e.g., action="order.php").
  • If the submit() function returns false, the browser does not send the data. It does nothing.

We can use this as follows:

$("#order_form").submit(function() {
  ...
  var data_ok = true;
  ...
  if ( [there is an error] ) {
    ...
    data_ok = false;
  }
  ...
  return data_ok;
});

Figure 9. Stopping form submission

The variable data_ok is set to true initially. If there is an error, data_ok is set to false. Eventually, the browser gets to the line:

return data_ok;

If there have been no errors, data_ok will still be set to true, the value it started off with. Return it, and the browser will send the data. But if there has been an error, data_ok will be false. Return it, and the browser will not send the data to the server.

Error reporting

Let’s look at the code the reports server-side errors to the user.

By the way, the variable $data_ok is a server-side PHP variable. It has nothing to do with the JavaScript variable data_ok.

//Flag for data OK.
$data_ok = true;
//JavaScript to run after page load.
$js = '';

//Check frisbees.
$frisbees_message = check_number_ordered($frisbees);
if ( $frisbees_message != '' ) {
  $data_ok = false;
  $js .= 'show_product_error_message("'.$frisbees_message . '", "frisbees");';
}

Figure 10. Error reporting

Most of it should be familiar, but there is one new thing. Line 22:

$js = '';

This PHP variable holds JavaScript code that reports errors to the user. Let’s see how it works.

Suppose the user entered “bark!” in the frisbees field. Line 25:

$frisbees_message = check_number_ordered($frisbees);

would put “Please enter a number” in $frisbees_message. Remember, this is PHP code, running on the server.

We want to give this error message to the user. We want it to look the same as the error messages produced by the client-side code.

How? Well, we already have a JavaScript function that will report error messages. We saw it on the previous page. Here it is again:

function show_product_error_message(message, product_name) {
  //Show the error message in the product's message area.
  $('#' + product_name + '_message').text(message);
  if ( $('#' + product_name + '_message_container').is(':hidden') ) {
    $('#' + product_name + '_message_container').show('medium');
  }
  //Show the global error message at the top of the page.
  show_global_error_message("Sorry, I can't process this order.");
}

Figure 11. JavaScript function to show an error message

show_product_error_message() takes two arguments: the error message, and the product name.

So, we already have this function that will report errors. But it’s a JavaScript function. It runs in the browser. We want to call it from PHP code that runs on the server.

How?

Well, how about writing PHP code that writes JavaScript code that calls the function?

Like this (line 28 in Figure 10):

$js .= 'show_product_error_message("'.$frisbees_message . '", "frisbees");';

Suppose $frisbees_message has ‘Please enter a number’ in it. We get the string:

show_product_error_message("Please enter a number", "frisbees");

The string is appended to the PHP variable $js.

We can do something similar for the on-hand check:

if ( $frisbees > $frisbees_on_hand ) {
  $data_ok = false;
  $js .= "show_product_error_message('Sorry, we only have $frisbees_on_hand on hand.', 'frisbees');";
}

Figure 12. Checking inventory on hand

Notice that we have:

$js .=

We keep adding to $js as we find errors.

What do we do with $js? If we could insert it into the page along with the other JavaScript, it would get executed, and the error message(s) would appear. Here’s code that would do it.

<script type="text/javascript">
$(document).ready(function() {
  $("#global_error_message_container").hide();
  $("#frisbees").focus();
  <?php print $js; ?>

Figure 13. Inserting JavaScript error reporting code

Line 100 puts the contents of the PHP variable $js into the output stream. The variable contains some executable JavaScript code. The code is added into the $(document).ready() (starts in line 97), so it runs when the page loads.

To see how this works, try it, and enter large numbers in the form fields:

Too many again

Figure 14. Too many again

When you get a display like this, have a look at the source code of the page. You’ll see something like this:

<script type="text/javascript">
$(document).ready(function() {
  $("#global_error_message_container").hide();
  $("#frisbees").focus();
  show_product_error_message('Sorry, we only have 5 on hand.', 'frisbees');show_product_error_message('Sorry, we only have 4 on hand.', 'giant_chew_ropes');show_product_error_message('Sorry, we only have 8 on hand.', 'squeaky_balls');

Figure 15. Generated code

Line 5 was generated by PHP code. But the browser doesn’t know where it came from. It’s just there, and gets run along with everything else.

CC
CC

I’m confused. The PHP adds JavaScript? Won’t it be part of the page forever, even when the user didn’t make a mistake?

Kieran
Kieran

Remember that order.php is a PHP program, not a static HTML file. order.php can send different HTML and JavaScript to the browser, depending on the circumstances.

So when it inserts JavaScript into the HTML stream, it doesn’t permanently change order.php itself. Just what the user sees that particular time order.php is run.

But this can be hard to understand. There’s a lot going on. Let’s break it down.

How the user experiences errors

We want to give users a smooth experience with the form. That’s what matters in the end: can users easily order products? They don’t care about clients and servers and such. They just care what they see in the browser.

Let’s see how one user, Jeff, experienced the order form. Keep this is mind:

Structure

Figure 16. Structure

Jeff
Jeff
Firefox
Browser
PHP
Server
Looking at site's home page.

Clicks link to go to order form.
   
  Asks server for order.php  
    Starts order.php.

No form data passed (1 in Figure 16).

Generate page to show empty form (3 in Figure 16).

Send HTML and JavaScript to browser.
  Render page from server. Blank form.

Blank form
 
Types "lots" into frisbee field.

Clicks Order button.

Lots
   
  Runs JavaScript validation code.

"lots" is not a valid number.

Reports error.

Lots error

Note: Nothing sent to the server. The browser handles this error itself.
 
Changes "lots" to 400.

Clicks Order button.
   
  Runs JavaScript validation code.

400 is a valid number.

Asks server for order.php

Sends data: frisbees=400
 
    Starts order.php.

Note: This is the same page! order.php sent data to itself.

Form data passed:
  frisbees=400

Note: The first time the browser asked for order.php, it passed no form data. This time is different.

Run validation check.

Only have 5 frisbees.

Generate HTML and JavaScript for page.

Add into $(document).ready():

show_product_error_message(
'Sorry, we only have 5 on hand.', 'frisbees');


Send HTML and JavaScript to browser.
  Render page.

Show error message during page load.

400 error

Note: This error looks the same as the "Invalid number" error. At this point, Jeff has only ever seen order.php. But the server has run order.php twice. The first run created a blank form. The second run created a page with an error message and a form.
 
Erases 400 from frisbees field.

Puts 4 into frisbees field.

Clicks Order button.
   
  Runs JavaScript validation code.

4 is a valid number.

Asks server for order.php

Sends data: frisbees=4

Note: Calling the same page again. Still on order.php.
 
    Starts order.php.

Form data passed:
  frisbees=4

Run validation check.

Data valid.

Generate HTTP response with header:

Location:process.php?

  frisbees=4


Send HTTP response to browser.
  Gets HTTP redirect from server.

Asks server for process.php

Note: Finally! Going to a different page.

Sends data: frisbees=4
 
    Starts process.php.

Form data passed:
  frisbees=4

Run validation check.

Data valid.

Process order. (Send messages to accounting, shipping, etc.)

Generate HTML for order confirmation page.

Send page to browser.
  Render confirmation page.  
W00f!

Prints confirmation page.
   

Try the page, and run through the same steps Jeff did.

An important note:

Jeff’s experience is seamless.

The order page reloads after Jeff enters 400 frisbees and clicks the Order button. But he might not even notice the reload, if the server is fast enough. The browser shows the same color and fonts and headings and fields.

The key to this is that the same program – order.php – creates different HTML and JavaScript, depending on the situation.

Restoring values

One last piece of code to look at. This is from the HTML that makes the order form.

<input type="text" name="frisbees" id="frisbees" size="3"
<?php
if ( isset($_POST['frisbees']) ) {
  print ' value="' . $_POST['frisbees'] . '"';
}
?>
>

Figure 17. <input> field

Look at the source code in your browser again, you’ll see:

<input type="text" name="frisbees" id="frisbees" size="3" value="100">

(I left out some spaces.)

You can see that it puts the value the user typed into the field.

But the other thing is that the <input> tag has both name and id attributes. Why both? The name is needed to send the form data to the server. The id is used by the client-side JavaScript code.

This is by far the most complex page we’ve talked about. To get a better sense of how the whole thing fits together, download the files.

Summary

This lessons shows a complete form validation example.

  • It shows good error messages.
  • Some error checking is on the server side. Some is on the client side.
  • Bother server-side and client-side error messages are reported in the same way. That’s done by writing PHP code that calls JavaScript code.

What now?

Time for more exercises.


How to...

Lessons

User login

Log in problems? Try here


Dogs