PHP Basic Form Validation Tutorial

In this tutorial we learn how to process forms with simple validations to enhance the security of our application.

What is form validation?

Form validation is the process of evaluating fields against specific requirements that we set.

For example, we may require a user’s password to be longer than 7 letters and contain uppercase, lowercase and special charcters as well as a number.

note There is no one-size-fits-all solution to form validation. It all depends on the type of fields you use, what you want to evaluate and how thorough you want your validation to be.

Simple form validation

In this lesson we will keep things simple and validate only text, email and password fields. But, the same concepts will apply across other form elements as well.

If you have been following along with the course, create a new file called form-validation.php in your /xampp/htdocs/PHPProjects/ folder. If not, any empty .php file will do.

Inside, create a simple form that submits to itself like we learned in the form handling lesson.

Example: simple form
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">

    <p><button>Submit</button></p>
</form>

We will write our PHP script directly below this form and add any other elements we need throughout the lesson.

How to validate form submission

In many cases a form redirects to a standalone processing page when submitted, so it’s safe to assume that a form submission has requested the page.

But because our form submits to its current page, we won’t know that it’s been submitted. An easy way to check is to see if the server has sent a request to the page.

For that we can use the $_SERVER['REQUEST_METHOD'] superglobal and test its result. If the result is the same as our form’s method, the form has been submitted.

Example:
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">

  <p><button>Submit</button></p>
</form>

<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Validations here
  echo "Form has been submitted";
}

?>

If we run the example in the browser and press the Submit button, it will show the message defined above.

We should only process the form if it has been submitted, so any further processing should be inside the if statement’s body.

note We can validate form submission even if the processing page is not the current page. It’s easy to do and adds some safety.

How to validate a field with a minimum character requirement

Now we will validate form fields so add a “username” text field with a label to the form.

Example: text field
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">

  <p>
    <label for="username">Name*</label><br>
    <input type="text" name="username">
  </p>

  <p><button>Submit</button></p>
</form>

Because we will be performing multiple validations, we should store the input we get for the field in a variable.

Example: store input
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Get & store username
  $username = $_POST['username'];
}

?>

For our example, we want the username to be a required input and to have a minimum of 3 characters.

So we want to use the following built-in PHP methods for the validation.

  • empty() will check if the field is empty or not.
  • strlen() will tell us how many characters a string has.

We can combine the two evaluations in the same condition block.

Example: not empty & greater than 3 chars
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username
  $username = $_POST['username'];

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  }
}

?>

If you run the example in the browser and submit the form with the empty field or containing less than 3 characters, it will print the error message.

How to validate an email field with PHP's built-in filter_var() method

Web applications often accept email addresses for user registration or subscriptions, so it’s important for us to validate email fields.

Let’s add an email field to our example form.

Example:
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
  <p>
    <label for="username">Name*</label><br>
    <input type="text" name="username">
  </p>

  <p>
    <label for="email">Email*</label><br>
    <input type="email" name="email">
  </p>

  <p><button>Submit</button></p>
</form>

PHP provides us with the filter_var() method to help perform validations through the use of filters.

As its first argument, the method expects the string we want to filter. The second argument is the filter we want to apply, which in this case is FILTER_VALIDATE_EMAIL .

Example: validate email filter
filter_var(email_address, FILTER_VALIDATE_EMAIL);

The filter_var() method will return true if the email is valid, and false if not. So our evaluation can be a simple ! (not) test.

We also want the email address to be required, so we can do an empty() check along with the filter check in the same condition block.

Example: empty and filter evaluation
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username
  $username = $_POST['username'];

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  }

  // Email
  $email = $_POST['email'];

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  }
}

?>

We will use the filter_var() method again later on when we sanitize the email input.

How to validate a password field with a regular expression (RegEx)

So far we’ve only done basic validation. But forms often require more advanced validation techniques, like testing for specific characters in a password.

As an example we will use a password field, so let’s add it to our form.

Example: password field
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
    <p>
      <label for="username">Name*</label><br>
      <input type="text" name="username">
    </p>

    <p>
      <label for="email">Email*</label><br>
      <input type="email" name="email">
    </p>

    <p>
      <label for="password">Password*</label><br>
      <input type="password" name="password">
    </p>

    <p><button>Submit</button></p>
</form>

For our example password, we want the following list of requirements.

  • A minimum of 7 characters.
  • A minimum of 1 uppercase letter.
  • A minimum of 1 number.
  • A minimum of 1 special character (!@#$%^&*-)

For such a validation, we will need to use a Regular Expression (RegEx).

But what is a regular expression?

A regular expression is a string of text that has been encoded in a certain way to help us find specific patterns in text. The patterns in our case are the uppercase letters, numbers and special characters.

Regular expressions can seem a little complicated at first, so we will build our RegEx step-by-step and explain each part as we go.

We will start by defining a variable called $pattern that will hold our regular expression. Remember, a RegEx is a string so we have to wrap it in quotes.

Example: variable to store our regex pattern
<?php

$pattern  = "";

?>

Delimiters:

The first elements in a regular expression are the delimiters. We specify them so that we don’t have to escape all the special characters that we’ll specify later on.

We specify a delimiter with a / (forward slash) at both the beginning and end of the pattern.

Example: delimiters
<?php

$pattern  = "/  /";

?>

note We use forward slashes as delimiters here, but PCRE (the syntax that PHP uses) also supports the following delimiters.

  • # (Hash Symbol)
  • % (Percentage Symbols)
  • + (Plus Symbol)
  • ~ (Tilde Symbol)


Start & End:

Next, we define the start and the end of the pattern. We use ^ for the start, and $ for the end.

Example: pattern start and end
<?php

$pattern  = "/^$/";

?>

Special Characters

We will start our evaluation with special characters.

Example: special characters
<?php

$pattern  = "/^(?=.*[!@#$%^&*-])$/";

?>

In the example above we added (?=.*[!@#$%^&*-]) , let’s break down each part of this pattern.

  1. The ?= at the start means we want to look ahead through the password.
  2. The .* means we want to look for any number of occurrences.
  3. The [] means we want whatever we specify inside to be looked for in the password.
  4. The !@#$%^&*- are the actual characters we want to look for.
  5. The parentheses are used as a wrapper to separate the pattern from any others.

We use this pattern often, only changing the characters we want to look for.

Numbers:

Numbers use the same pattern as the special characters. The only difference is we specify a range between 0 and 9, separated with the - (dash) symbol.

Example: numbers
<?php

$pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])$/";

?>

This time we added (?=.*[0-9]) . It’s the same as with the special characters, but we specify a range of numbers.

We chose to add the full range but if you want you can have a custom range, like [3-6] .

Letters:

Letters use the same pattern as numbers, with a range between A and Z.

Letter patterns come in two flavors though, uppercase and lowercase. For uppercase, we specify the range as uppercase letters [A-Z] and for lowercase, we specify the range as lowercase letters [a-z] .

In our case we want to check for uppercase letters, so let’s add it to the example.

Example: uppercase letters
<?php

$pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z])$/";

?>

This time we added (?=.*[A-Z]) .

Minimum Characters

A regular expression can also test for a min and max amount of characters with .{min,max} .

In our case, we want a minimum of 7 characters, so let’s add it to the example.

Example: minimum amount of characters
<?php

$pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

?>

We added .{7,} .

  1. As we know, the . means any character.
  2. For the minimum we specified 7.
  3. For the maximum we didn’t specify a number, which means it can be any amount.

If we want to specify that the password must be exactly 7 characters, we don’t have to add the comma. If we want to specify a maximum, we just add a maximum number after the comma.

The full pattern is complete.

The next thing we need to do is test the $pattern against the $password . For this we can use the preg_match() method in PHP, which is specially designed to match regular expressions against strings.

The preg_match() method takes the pattern as the first argument, and the string we want to test against as the second argument.

Syntax: preg_match()
<?php

preg_match($pattern, $password);

?>

The method will return 1 if the pattern matches, 0 if it does not, or false if an error occurred.

The password is also a required field, so we can do an empty() check along with the RegEx match in the same condition block.

note As mentioned before, the method returns numbers if the evaluation is true or false, not the actual booleans. So we shouldn’t test for a truthy value, we should explicitly test for a 1 or 0 value.

We use === instead of == to match the type as well as the value.

Example: advanced password validation
<?php

// Password
$password = $_POST['password'];

// Define RegEx pattern
$pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

// Validate empty or pattern
if(empty($password) || preg_match($pattern, $password) === 0) {
  echo "<p>Error: Password is invalid</p>";
}

?>

The full script, with all the previous validations, should now look as follows.

Example: all validations
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username
  $username = $_POST['username'];

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  }

  // Email
  $email = $_POST['email'];

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  }

  // Password
  $password = $_POST['password'];

  // Define RegEx pattern
  $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

  // Validate empty or pattern
  if(empty($password) || preg_match($pattern, $password) === 0) {
    echo "<p>Error: Password is invalid</p>";
  }
}

?>

We can test variations of a password that meet the requirements in the browser. Each time the password doesn’t match one of the requirements, it will show the error message.

tip For a better user experience, we could let the user know which part of their password fails the requirement.

For that we would create separate patterns and test each in an else..if ladder with a message each time a part of the pattern fails.

Example: partial patterns
<?php

// Empty
empty($password);

// Special character
$pattern1 = "/^(?=.*[!@#$%^&*-])$/";

// Number
$pattern2 = "/^(?=.*[0-9])$/";

// Uppercase letter
$pattern3 = "/^(?=.*[A-Z])$/";

// Length
$pattern4 = "/^.{7,60}$/";

?>

note It’s important to note that we’re only validating input based on rules we define. At this point, we’re not sanitizing the input for safety yet.

It’s recommended that regular-expression methods like the preg_* family should not be used to sanitize inputs.

How to clean up and sanitize form input

We should always clean up and sanitize any input coming from a form, before sending it to our application’s storage layer.

We’ll break the process into two parts.

  • Pre-validation, that cleans the input before the validations from above are done.
  • Post-validation, that sanitizes the input before it goes into our application’s storage layer.

Because we are going to use the logic for each input, let’s store it into two functions called preVal() and postVal() . Both methods will accept a string as parameter and return the cleaned or sanitized string.

The preVal() function:

In the preVal() function, we’ll use PHP’s trim() method to remove any whitespace the user may have accidentally added at the beginning or end.

Example: preVal
function preVal($str) {
    return trim($str);
}

As mentioned before, we use it before the validations are done. So we can invoke the function when we get the input and store it into a variable.

Example: clean up inputs
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username & cleanup
  $username = preVal($_POST['username']);

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  }

  // Email & cleanup
  $email = preVal($_POST['email']);

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  }

  // Password & cleanup
  $password = preVal($_POST['password']);

  // Define RegEx pattern
  $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

  // Validate empty or pattern
  if(empty($password) || preg_match($pattern, $password) === 0) {
    echo "<p>Error: Password is invalid</p>";
  }
}

// Clean up
function preVal($str) {
  return trim($str);
}

?>

The postVal() function:

In the postVal() function, we’ll use PHP’s htmlentities() method to replace all HTML characters with their entity equivalents.

Example: postVal
function postVal($str) {
    return htmlentities($str);
}

By default the method will not escape single quotes, we have to explicitly add the ENT_QUOTES flag as a second argument to ensure it does escape single quotes.

We should also specify an encoding character set as the third argument. This will typically be the same as the page’s <meta charset="utf-8"> tag.

Example: optimal postVal
function postVal($str) {
    return htmlentities($str, ENT_QUOTES, 'UTF-8');
}

We want to escape input that has already passed validation and is about to go into storage. So we can invoke the method in the else clause of our conditional statements.

Example: sanitize inputs
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

    // Username & cleanup
    $username = preVal($_POST['username']);

    // Validate empty or less than 3 characters
    if(empty($username) || strlen($username) < 3) {
      echo "<p>Error: Name requires a minimum of 3 characters</p>";
    } else {
      $username = postVal($username);
    }

    // Email & cleanup
    $email = preVal($_POST['email']);

    // Validate empty or email
    if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
      echo "<p>Error: Please provide a valid email address</p>";
    } else {
      $email = postVal($email);
    }

    // Password & cleanup
    $password = preVal($_POST['password']);

    // Define RegEx pattern
    $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

    // Validate empty or pattern
    if(empty($password) || preg_match($pattern, $password) === 0) {
      echo "<p>Error: Password is invalid</p>";
    } else {
      $password = postVal($password);
    }

}

// Clean up
function preVal($str) {
  return trim($str);
}
// Escape HTML
function postVal($str) {
  return htmlentities($str, ENT_QUOTES, 'UTF-8');
}

?>

tip The method (and the else statement) is also a great place to perform other security actions like preparing PDO statements or hashing a password.

Bonus: Password Hashing

PHP provides us with the built-in password_hash() method to encrypt our passwords.

It’s not really encryption because encryption goes two ways, it can be decrypted. Hashing is designed to be a safer one-way conversion.

Passwords are never stored as what the user inputs. Instead we hash the password so that if a hacker gains access to the storage system, they only see hashes, which would be meaningless to them.

When a user uses their password to log in, the password is hashed again and compared to the one in storage.

Hashing relies on a hashing algorithm like MD5, SHA1 or bcrypt, which is the most secure hashing algorithm at this time. The bcrypt algorithm takes a large amount of time (compared to other algorithms like MD5) to iteratively hash its data. It also salts the data to protect against potential rainbow table attacks.

The number of those hash iterations is known as the work factor. More iterations make the hash more secure, but takes longer. That is also why the bcrypt algorithm is future-proof, we can simply increase the work factor as our systems become more powerful.

As mentioned before, we use PHP’s built-in password_hash() method to hash a password.

As its first argument, the method expects the password input that will be hashed. The second argument is the algorithm we want to use and the third (optional) argument is an array containing options, like the work factor.

Syntax: password_hash()
password_hash($password, PASSWORD_DEFAULT, ['cost' => 12])

The PASSWORD_DEFAULT flag is PHP’s default algorithm, which is bcrypt. However, this may change in the future so we can also specify PASSWORD_BCRYPT explicitly.

The options for the bcrypt algorithm is the work factor, specified as cost . The default value is 10, which is a decent starting value but we can increase it manually based on our system’s performance. For our example, we will leave it at the default and so not include the option as argument at all.

Because the password will be hashed, we don’t have to worry about escaping characters in the postVal() method. We can either remove it, or do the password hashing after it.

Example: password_hash()
// Validate empty or pattern
if(empty($password) || preg_match($pattern, $password) === 0) {
  echo "<p>Error: Password is invalid</p>";
} else {
  $password = postVal($password);

  // Hash password
  $passwordHash = password_hash($password, PASSWORD_DEFAULT);
}

Notice that we stored the hashed password in a different variable. That’s because we want to verify that the password matches the hashed password before it goes into storage.

To do that, we use PHP’s built-in password_verify() method. This method accepts the password as the first argument, the hash as the second argument and will return true if they match.

Only when they match do we want to replace the $password variable that goes into storage, with the hashed password.

Example: password_hash()
// Validate empty or pattern
if(empty($password) || preg_match($pattern, $password) === 0) {
  echo "<p>Error: Password is invalid</p>";
} else {
  $password = postVal($password);

  // Hash password
  $passwordHash = password_hash($password, PASSWORD_DEFAULT);

  // Verify hash
  if (password_verify($password, $passwordHash)) {
    $password = $passwordHash;
  } else {
    echo "<p>Error: Password is invalid</p>";
  }
}

The full script with all previous validations and sanitizing should now look as follows.

Example:
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username & cleanup
  $username = preVal($_POST['username']);

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  } else {
    $username = postVal($username);
  }

  // Email & cleanup
  $email = preVal($_POST['email']);

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  } else {
    $email = postVal($email);
  }

  // Password & cleanup
  $password = preVal($_POST['password']);

  // Define RegEx pattern
  $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

  // Validate empty or pattern
  if(empty($password) || preg_match($pattern, $password) === 0) {
    echo "<p>Error: Password is invalid</p>";
  } else {
    $password = postVal($password);

    // Hash password
    $passwordHash = password_hash($password, PASSWORD_DEFAULT);

    // Verify hash
    if (password_verify($password, $passwordHash)) {
      $password = $passwordHash;
    } else {
      echo "<p>Error: Password is invalid</p>";
    }
  }
}

// Clean up
function preVal($str) {
  return trim($str);
}
// Escape HTML
function postVal($str) {
  return htmlentities($str, ENT_QUOTES, 'UTF-8');
}

?>

Additional sanitizing with filter_var()

PHP provides us with the filter_var() method, which accepts a variety of filters to validate and sanitize different forms of input such as emails, URLs etc.

We used this method with a validation filter earlier in the lesson when we validated the email input.

Example: email validation
$email = $_POST['email'];
// Validate email
if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {}

Now we’re going to use it with sanitizing filters to add extra sanitizing to two our form elements.

Sanitize Email:

The email address will be the easiest, so let’s start with that. All we have to do is specify the input and the FILTER_SANITIZE_EMAIL flag as arguments to the method.

When the flag is applied, the method will remove all characters except letters, digits and the following special characters: !#$%&'*+-=?^_{|}~@.[] .

So we can invoke the method in the else statement before the postVal() method.

Example: email sanitize
// Validate empty or email
if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
  echo "<p>Error: Please provide a valid email address</p>";
} else {
  $email = filter_var($email, FILTER_SANITIZE_EMAIL);
  $email = postVal($email);
}

Sanitize international characters (with accents):

Many international languages like French or Spanish use special characters with accents as letters. For example: Lëàrñ tø çôdê wïth KødérHQ

PHP allows us to sanitize characters in certain ranges of the ASCII table .

In our case, we want to remove characters like tab and backspace (below 32 on the table), and escape characters with special accents like ëàñçô (above 127 on the table).

We want this for all the inputs in our example form, so we can do it in the postVal() function before htmlentities() .

To sanitize the accented characters, we use the FILTER_SANITIZE_STRING flag, as well as two filter flags for the ASCII table, separated with a | (pipe) symbol.

  • FILTER_FLAG_STRIP_LOW to remove characters below 32.
  • FILTER_FLAG_ENCODE_HIGH to escape characters above 127.

note For a good user experience, we don’t want to strip the accented characters, just encode them.

Example: sanitize accented characters
function postVal($str) {
  // Remove below 32, escape above 127
  $safe = filter_var(
    $str,
    FILTER_SANITIZE_STRING,
    FILTER_FLAG_STRIP_LOW|FILTER_FLAG_ENCODE_HIGH
    );

  // Escape HTML
  return htmlentities($safe, ENT_QUOTES, 'UTF-8');
}

Our fully sanitized, validated and hashed script should now look like the following.

Example:
<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username & cleanup
  $username = preVal($_POST['username']);

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  } else {
    $username = postVal($username);
  }

  // Email & cleanup
  $email = preVal($_POST['email']);

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  } else {
    $email = filter_var($email, FILTER_SANITIZE_EMAIL);
    $email = postVal($email);
  }

  // Password & cleanup
  $password = preVal($_POST['password']);

  // Define RegEx pattern
  $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

  // Validate empty or pattern
  if(empty($password) || preg_match($pattern, $password) === 0) {
    echo "<p>Error: Password is invalid</p>";
  } else {
    $password = postVal($password);

    // Hash password
    $passwordHash = password_hash($password, PASSWORD_DEFAULT);

    // Verify hash
    if (password_verify($password, $passwordHash)) {
      $password = $passwordHash;
    } else {
      echo "<p>Error: Password is invalid</p>";
    }
  }
}

// Clean up
function preVal($str) {
  return trim($str);
}

function postVal($str) {
  // Remove below 32, escape above 127
  $safe = filter_var(
    $str,
    FILTER_SANITIZE_STRING,
    FILTER_FLAG_STRIP_LOW|FILTER_FLAG_ENCODE_HIGH
    );

  // Escape HTML
  return htmlentities($safe, ENT_QUOTES, 'UTF-8');
}

?>

How to make your inputs sticky

Sticky inputs are input fields that remember any previously entered data if the user makes a mistake. It’s likely that you have encountered this annoying behavior before, submitting a form only to have to re-enter all the information because of a simple mistake.

The solution is quite simple. We first check if the user has entered any data, if so, we echo the data back to the form.

Example:
<?php

if(isset($_POST['username'])) {
    echo $_POST['username'];
}

?>

This is done within the value="" attribute of the html input field.

Example: inline sticky input
<input
  type="text"
  name="username"
  value="<?php if(isset($_POST['username'])) echo $_POST['username']; ?>"
  >

Even though you can make password fields sticky, we strongly recommend not doing so.

Example:
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">

    <p>
      <label for="username">Name*</label><br>
      <input
        type="text"
        name="username"
        value="<?php if(isset($_POST['username'])) echo $_POST['username']; ?>"
        >
    </p>

    <p>
      <label for="email">Email*</label><br>
      <input
        type="email"
        name="email"
        value="<?php if(isset($_POST['email'])) echo $_POST['email']; ?>"
        >
    </p>

    <p>
      <label for="password">Password*</label><br>
      <input type="password" name="password">
    </p>

    <p><button>Submit</button></p>
</form>

Most websites and applications have sticky form data. If a form is short and uncomplicated, users may not mind that much when it’s not sticky. If a form is longer or more complicated, most users will simply leave the site instead of refilling the form.

Our full page should now look like the following.

Example:
<html>
<body>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
  <p>
    <label for="username">Name*</label><br>
    <input
      type="text"
      name="username"
      value="<?php if(isset($_POST['username'])) echo $_POST['username']; ?>"
      >
  </p>

  <p>
    <label for="email">Email*</label><br>
    <input
      type="email"
      name="email"
      value="<?php if(isset($_POST['email'])) echo $_POST['email']; ?>"
      >
  </p>

  <p>
    <label for="password">Password*</label><br>
    <input type="password" name="password">
  </p>

  <p><button>Submit</button></p>
</form>

<?php

// Has the form been submitted
if($_SERVER['REQUEST_METHOD'] == 'POST') {

  // Username & cleanup
  $username = preVal($_POST['username']);

  // Validate empty or less than 3 characters
  if(empty($username) || strlen($username) < 3) {
    echo "<p>Error: Name requires a minimum of 3 characters</p>";
  } else {
    $username = postVal($username);
  }

  // Email & cleanup
  $email = preVal($_POST['email']);

  // Validate empty or email
  if(empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "<p>Error: Please provide a valid email address</p>";
  } else {
    $email = filter_var($email, FILTER_SANITIZE_EMAIL);
    $email = postVal($email);
  }

  // Password & cleanup
  $password = preVal($_POST['password']);

  // Define RegEx pattern
  $pattern  = "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/";

  // Validate empty or pattern
  if(empty($password) || preg_match($pattern, $password) === 0) {
    echo "<p>Error: Password is invalid</p>";
  } else {
    $password = postVal($password);

    // Hash password
    $passwordHash = password_hash($password, PASSWORD_DEFAULT);

    // Verify hash
    if (password_verify($password, $passwordHash)) {
      $password = $passwordHash;
    } else {
      echo "<p>Error: Password is invalid</p>";
    }
  }
}

// Clean up
function preVal($str) {
  return trim($str);
}

function postVal($str) {
  // Remove below 32, escape above 127
  $safe = filter_var(
    $str,
    FILTER_SANITIZE_STRING,
    FILTER_FLAG_STRIP_LOW|FILTER_FLAG_ENCODE_HIGH
    );

  // Escape HTML
  return htmlentities($safe, ENT_QUOTES, 'UTF-8');
}

?>

Summary: Points to remember

  • Form validation is the process of evaluating fields against specific requirements.
    • There is no single solution for form validation. Every developer will have different needs.
  • We should validate that our form was submitted with the $_SERVER['REQUEST_METHOD'] superglobal.
  • When a field is required, we can check if the user input a value with the built-in empty() method.
  • When a field has a minimum length requirement, we can get the length with the built-in strlen() method and test against it with an if statement.
  • An email address can be validated with the filter_var() method and FILTER_VALIDATE_EMAIL flag.
  • An email address can be sanitized with the filter_var() method and FILTER_SANITIZE_EMAIL flag.
  • Characters with accents can be escaped with the filter_var() method and FILTER_SANITIZE_STRING .
    • Additionally, we need to remove all characters below 32 on the ASCII table with FILTER_FLAG_STRIP_LOW and escape characters above 127 with FILTER_FLAG_ENCODE_HIGH .
  • A password field with special requirements can be validated with a regular expression (RegEx).
    • Typical requirements are at least 7 or 8 characters long, and at least one number, uppercase letter and special character. It’s regex would look similar to this: "/^(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[A-Z]).{7,}$/" .
    • For a good user experience we can spilt up the regex pattern to test them individually and inform the user which part of their password does not meet the requirement.
  • A password can be hashed with the built-in password_hash() method.
    • The default algorithm for PHP at this time is bcrypt and the default amount of hash iterations is 10.
    • Passwords should be verified against their hashes with the built-in password_verify() method.
  • We can allow our fields to “remember” the data a user inputs by echoing back the data into the field’s value="" attribute.