There are more than a single way to handle HTML forms. These days, the prevalent way is to handle form submissions completely using Javascript by using preventDefault() to prevent the default behavior, then use FetchAPI to pass it to the backend or API.

You can even put input elements outside the form element, then use event listeners to watch for a button click or other user interaction (although it’s recommended to use <form> as it is semantically more correct).

The advantage is the ability to submit forms without refreshing the page.

However, the default behavior of an HTML form is to submit the field values inside it when the user interacts with the <input type="submit"> or <button>. It can be:

  • mouse click
  • keyboard action like pressing the enter key
  • or other commands

So in this post, let’s see how we can handle forms using pure HTML on the client side and PHP on the server side.

If you prefer watching a video, here is a detailed one:

For that, first you need to have a good understanding about the HTML Form element.

The Form Element

A form is primarily a way to collect inputs from users. It can contain several elements inside it, such as:

  • input - text, email, number, etc - <input type="email">
  • select boxes and options - <select>
  • radio buttons - <input type="radio">
  • checkboxes - <input type="checkbox">
  • text fields - <textarea>

The form element also supports a couple of attributes, out of which the most important ones we need to know are:

  • action
  • method

Understanding the action attribute

The action attribute determines where the data should be send when submitted. You can put a relative or absolute URL.

<form action="/some/page.php">
 <!-- elements inside form -->
</form>

If the action attribute is not specified or if the value is empty, then the form will be submitted to the same page.

Understanding the method attribute

Use the method attribute to set the request type. post and get are the two common values.

A get request is usually used when it’s okay to sent the form data with the URL. The data is appended as query strings at the end of the URL mentioned in the action attribute.

2048 characters is the maximum limit set by most browsers.

Whereas with a post request, the data is sent with the request body, after the HTTP headers. Unlike get requests, there are no length limits.

Since the data is passed with the request body instead of URL query strings, it is often perceived as more secure. For instance, if someone is screen-recording a login page, get request can reveal the in the address bar to anyone who views the video. While post data is not directly visible unless you check the headers in the Developer Tools.

As a general rule, we use get requests for reading something, such as product filtering, pagination, etc. You might have noticed product.php?order=asc&color=blue&sortby=name or some.php?page=2, etc.

<form action="handler.php" method="get">
    <!-- elements inside form -->
</form>

Post request is generally used for creating, deleting, updating, etc, where the data on the server needs modification.

<form action="handler.php" method="post">
    <!-- elements inside form -->
</form>

If method attribute is empty or not used, then the default is get.

Significance of name attribute for form elements

The name attribute is used to identify submitted form data and group form elements together.

For instance, take the case of this input element:

<form action="handler.php" method="get">
    <input type="text" name="color">
</form>

When it’s submitted as a get request, the name attribute becomes the query string - some.php?color=red. The value will be accessible to PHP using $_GET['color'] or $_REQUEST['color'].

Else if the it is a post request, then PHP can access it using $_POST['color'] or $_REQUEST['color'];

  • You can also use the same name value for multiple fields to group them together. Radio box inputs are an example.
  • You can add square brackets to accept multiple values as an array, as in the case of checkboxes.
<input type="checkbox" name="hobbies[]" value="Photography">
<input type="checkbox" name="hobbies[]" value="Travel">

PHP can then access all the checked values as an array:

[
    'hobbies' => [
        'Photography',
        'Travel'
    ]
]

Without the square brackets, only one of the values will be submitted if there are multiple fields with the same name.

label tag and for attribute

label tags and for attributes are not compulsory, but it’s a good practice to use them for better accessibility and user experience.

The for attribute value should be the id of the element it represents.

<input type="checkbox" id="photography" name="hobbies[]" value="Photography">
<label for="photography">Photography</label>
<input type="checkbox" id="travel" name="hobbies[]" value="Travel">
<label for="travel">Travel</label>

label also expands the clickable area of checkboxes and radio buttons, thereby giving a better user experience. Users can click either the box or the text to select that option.

Form Field Values in PHP

PHP offers the following super global variables to access submitted form data:

  • $_POST - contains the data submitted as a post request
  • $_GET - contains the data submitted as a get request
  • $_REQUEST - contains both post and get request data in addition to cookie data
  • $_FILES - contains files uploaded as part of a post request

In most cases, use either $_POST or $_GET instead of $_REQUEST.

Form Security in PHP

There are multiple vulnerabilities you should watch out for when developing an application that handles form data. A few of the them are:

  • Cross-site Scripting or XSS
  • SQL Injection
  • Cross-site Request Forgery or CSRF

And the measures you should take to prevent them are:

  • Data validation: making sure the data is not bogus.
  • Data sanitization: sanitize the data to make it unharmful.
  • Prepared statements to prevent SQL Injection
  • Output escaping to prevent XSS attacks
  • Form tokens to prevent CSRF attacks

Validating Submitted Data

Unexpected or harmful data can come in many forms. It can be intentional or unintentional. Examples include manipulating checkbox/select field options using the browser inspect tool, entering text in a number field, entering invalid email address, etc.

Validation makes sure the data falls in the expected range.

Let’s discuss some built-in PHP methods to validate data:

array_in() for values in a list

PHP’s array_in() is very helpful to check the submitted value against a list of items. You can use them to validate select boxes, radio button inputs, and checkboxes, where we can clearly whitelist the data.

$allowed_colors = ["red", "green", "blue"];
$submitted_color = $_POST['color'];

if(!array_in($submitted_color, $allowed_colors)) {
    $status = "Invalid color";
    /*
    exit / warn the user / log error
    */
}
else {
    $status = "Your chosen color is " . $submitted_color;
}

filter_var()

PHP offers some built-in filters to be used with the filter_var() function to validate several types of data, including emails, URLs, domain names, IP addresses, mac addresses, regular expressions, floats, ints, and booleans. FILTER_VALIDATE_EMAIL is one of them.

You can see the details on this page.

For instance, if you want to validate an email address, then you can do it like this:

$email = $_POST['email'];

$validated_email = filter_var($email, FILTER_VALIDATE_EMAIL);

if ($validated_email) {
    echo "email is valid";
}
else {
    echo "invalid email";
}

If it’s a valid email, then the function returns the email as a string, otherwise it returns boolean false.

ctype_alpha(), ctype_alnum() and others

Apart from the filter_var() function, there are also a couple of other functions like ctype_alpha() and ctype_alnum(). Both are helpful when you want to accept only alphabets or alphanumeric characters.

Usernames are a great example where you usually accept only alphanumeric characters.

Data sanitizing

Validation and sanitization are two different things. Validation only checks the data without modifying it while sanitization modifies it to make it unharmful.

e.g., stripping HTML tags, escaping quotes, etc.

The filter_var() function we discussed above offers a few sanitize filters as well in addition to validation filters.

Take the case of the emails:

$email = $_POST['email'];
$sanitized_email = filter_var($email, FILTER_SANITIZE_EMAIL);
// removes unnecessary characters like (, ), <, >, etc. that shouldn't appear in an email address. 

That’s not all. You can employ different techniques to make the data as sanitized as possible. str_replace(), preg_replace(), etc are some other useful functions that help to weed out unnecessary characters out of your data.

Prepared Statements

Most applications have a database layer from where you store and retrieve data. And most PHP applications use MySQL or MariaDB as the database management system. SQL databases are vulnerable to SQL injection attacks if you don’t sanitize the data enough.

Prepared statements and parameterized queries are kind of silver bullets to prevent SQL injections.

$connection = new PDO('mysql:dbname=myapp;host=localhost', "myuser", "mypass", [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

// prepare the statement once

$stmt = $connection->prepare("INSERT INTO people (fname, email) values (:fname, :email)");

// binding and executing multiple times

$fname = "Ramesh";
$email = "ramesh@example.com";

$stmt->bindParam(':fname', $fname, PDO::PARAM_STR);
$stmt->bindParam(':email', $email, PDO::PARAM_STR);

$stmt->execute();

$fname = "Suresh";
$email = "suresh@example.com";

$stmt->bindParam(':fname', $fname, PDO::PARAM_STR);
$stmt->bindParam(':email', $email, PDO::PARAM_STR);

$stmt->execute();

In the above code, you can see two things:

  1. First we prepared the statement like a template using PHP PDO. In place of values, we inserted named placeholders.
  2. We executed the same query multiple times with different values.

It offers two advantages:

  1. The SQL statement is prepared in advance before it receives any values or gets executed. So mischiefs like ramesh; DROP TABLE people won’t work.
  2. Since we can execute the same statement multiple times by just swapping the data, it offers performance benefits.

Output Escaping

Output escaping is mainly done to protect the users who access our website or application. We can use functions like htmlentities() and htmlspecialchars() to escape HTML tags, quotes, etc.

This prevents harmful Javascript execution on the user’s browser.

Imagine we’re building a blog which accepts user comments. Suppose someone enters the following piece of code instead of a genuine comment, below one of the blog posts. Since we’re echoing it as it is, the code gets executed for anyone who visits that page. Here it displays an alert box, but in reality it can steal data from the visitor’s browser.

$userComment = '<script>function doHarm(){alert("harm");}; doHarm();</script>';

echo $userComment; // outputting unescaped string

After escaping, the code won’t be executed. Instead it will be echoed as a normal text string.

// let's define a function so that it will be more readable when reused

function esc_str($str) {

    $esc_str = htmlentities($str, ENT_QUOTES, "UTF-8");
    return $esc_str;
}

$userComment = '<script>function doHarm(){alert("This is something nefarious");}; doHarm();</script>';

echo esc_str($userComment); // outputting escaped string

Note: It’s better not to escape the same data twice. For instance, if you save the escaped string to the database, then escaping it again before outputting can make the string look ugly. Generally, you escape all data before outputting. Data may not always come from the database. So trying to figure out whether it is already escaped or not can cause mistakes.

Form Tokens

Cross-site Request Forgery occurs when someone submits a form on behalf of a logged in visitor.

For instance, a malicious person directs one of your visitors to some random web page while they are logged in to your website. Maybe the user got tricked into clicking a link or something like that.

On that third-party page, there is a hidden form that sends a request to your form handler asking to delete the the account/purchase something on behalf of the user.

Your PHP script has no way to identify the origin of the request - whether it’s from your website’s frontend, or if it’s from somewhere else. Thinking that it’s a genuine request, your backend handles the request, sabotaging the user.

So, to prevent that, your backend needs a way to identify forms on its frontend.

That’s what form tokens do. A token is generated on the server, saved in the session, and then sent along with the HTML markup.

You can do something like this to generate unique tokens:

<?php
$token = md5(uniqid(microtime(), true));
$_SESSION['token'] = $token;
?>

<form action="/handler.php" method="post">
    <input type="hidden" name="token" value="<?php echo $token; ?>">
    <input type="text" name="userid" value="123">
    <button type="submit">Delete</button>
</form>

Conclusion

I hope this post helped you to get a basic understanding on form handling in PHP.