PHP PRG (Post-Redirect-Get)

Summary: in this tutorial, you’ll learn how to use the PHP PRG (Post-Redirect-Get) technique to prevent the double form submission problem.

The double submit problem

To change data on the server via a form, you often use the post method. When a form is submitted, you validate the data, update the database, and display the output.

However, if you click the Refresh (or Reload) button of the browser after the form is submitted, the browser will submit the form again.

Since the form uses the POST method, the browser will prompt for confirmation like this:

PHP PRG - Form Resubmission Problem

If you click the Continue button, the browser will actually submit the form a second time. It’s a notorious double submit problem that may cause many issues.

For example, you may have duplicate records in the database. If the form processes the payment, it’ll charge the customer twice.

The following diagram illustrates the double submit problem:

Introduction to the PHP PRG (post-redirect-get) technique

The PRG (post-redirect-get) technique helps you solve the double submit problem. The post-redirect-get works as follows:

  • First, the form is submitted using the HTTP POST method.
  • Second, the server does something, e.g., updates the database, processes a payment, sends an email, and redirects the browser to the result page.
  • Third, the browser makes the second request to the result page using HTTP GET method.
  • Finally, the server returns the result page and the browser displays it.

If the user refreshes the web browser, the browser will request the result page using the HTTP GET method.

Note that the result page and the page that contains the form can be the same.

The following diagram illustrates how the PRG works:

Illustrating the double submit problem

We’ll create a simple donation form that illustrates the double submit problem:

<?php const MIN_DONATION = 1; $errors = []; $inputs = []; $valid = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // sanitize & validate amount $amount = filter_input(INPUT_POST, 'amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); $inputs['amount'] = $amount; if ($amount !== false && $amount !== null) { $amount = filter_var($amount, FILTER_VALIDATE_FLOAT, ['options' => ['min_range' => MIN_DONATION]]); if ($amount === false) { $errors['amount'] = 'The minimum donation is $1'; } else { $valid = true; } } else { $errors['amount'] = 'Please enter a donation amount'; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="css/style.css"> <title>PHP - without PRG</title> </head> <body> <main> <form action="index.php" method="post"> <h1>Donation</h1> <?php if ($valid) : ?> <div class="alert alert-success"> Thank you for your donation of $<?= $inputs['amount'] ?? '' ?> </div> <?php endif ?> <div> <label for="amount">Amount:</label> <input type="text" name="amount" value="<?= $inputs['amount'] ?? '' ?>" id="amount" placeholder="Minimum donation $<?= MIN_DONATION ?>"> <small><?= $errors['amount'] ?? '' ?></small> </div> <button type="submit">Donate</button> </form> </main> </body> </html>
Code language: PHP (php)

How it works.

The form contains one input field that accepts a positive amount with a value greater than 1.

The MIN_DONATION constant specifies the minimum value of the amount field:

const MIN_DONATION = 1;
Code language: PHP (php)

When the form is submitted using the post method, we sanitize and validate the amount using the filter_input() and filter_var() functions.

The following sanitizes the amount value from the INPUT_POST:

$amount = filter_input(INPUT_POST, 'amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
Code language: PHP (php)

Notice that if you don’t specify the FILTER_FLAG_ALLOW_FRACTION flag, the function will remove the decimal point (.) from the entered value.

If the amount doesn’t exist in the INPUT_POST, the filter_input() returns null. If the amount is not a valid float, the filter_input() function returns false.

The following checks if the amount is a valid float and greater than or equal to 1:

$amount = filter_var($amount, FILTER_VALIDATE_FLOAT, ['options' => ['min_range' => MIN_DONATION]]);
Code language: PHP (php)

If the amount is valid, we set the $valid variable to true. Otherwise, we add the error messages to the $errors array. Also, we add the filtered value to the $inputs array.

if ($_SERVER['REQUEST_METHOD'] === 'POST') { // sanitize & validate amount $amount = filter_input(INPUT_POST, 'amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); $inputs['amount'] = $amount; if ($amount !== false && $amount !== null) { $amount = filter_var($amount, FILTER_VALIDATE_FLOAT, ['options' => ['min_range' => MIN_DONATION]]); if ($amount === false) { $errors['amount'] = 'The minimum donation is $1'; } else { $valid = true; } } else { $errors['amount'] = 'Please enter a donation amount'; } }
Code language: PHP (php)

In the form, we show a success message if the amount field is valid. Otherwise, we show the entered value with an error message.

<form action="index.php" method="post"> <h1>Donation</h1> <?php if ($valid) : ?> <div class="alert alert-success"> Thank you for your donation of $<?= $inputs['amount'] ?? '' ?> </div> <?php endif ?> <div> <label for="amount">Amount:</label> <input type="text" name="amount" value="<?= $inputs['amount'] ?? '' ?>" id="amount" placeholder="Minimum donation $<?= MIN_DONATION ?>"> <small><?= $errors['amount'] ?? '' ?></small> </div> <button type="submit">Donate</button> </form>
Code language: HTML, XML (xml)

To demonstrate the double submit problem, you can enter a valid value and click the Donate button. Once you see the success message, you can click the Refresh button on the web browser.

Fix the double form submit problem using the PRG technique

The following illustrates how to use the PRG technique to fix the double submit problem:

<?php session_start(); const MIN_DONATION = 1; $errors = []; $inputs = []; $valid = false; $request_method = strtoupper($_SERVER['REQUEST_METHOD']); if ($request_method === 'POST') { // sanitize & validate amount $amount = filter_input(INPUT_POST, 'amount', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); $inputs['amount'] = $amount; if ($amount !== false && $amount !== null) { $amount = filter_var($amount, FILTER_VALIDATE_FLOAT, ['options' => ['min_range' => MIN_DONATION]]); if ($amount === false) { $errors['amount'] = 'The minimum donation is $1'; } else { $valid = true; } } else { $errors['amount'] = 'Please enter a donation amount'; } // process the payment // ... // place variables to sessions $_SESSION['valid'] = $valid; $_SESSION['errors'] = $errors; $_SESSION['inputs'] = $inputs; // redirect to the page itself header('Location: index2.php', true, 303); exit; } elseif ($request_method === 'GET') { if (isset($_SESSION['valid'])) { // get the valid state from the session $valid = $_SESSION['valid']; unset($_SESSION['valid']); } if (isset($_SESSION['errors'])) { $errors = $_SESSION['errors']; unset($_SESSION['errors']); } if (isset($_SESSION['inputs'])) { $errors = $_SESSION['inputs']; unset($_SESSION['inputs']); } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="css/style.css"> <title>PHP PRG</title> </head> <body> <main> <?php if ($valid) : ?> <div class="alert alert-success"> Thank you for your donation of $<?= $inputs['amount'] ?? '' ?> </div> <?php endif ?> <form action="index.php" method="post"> <h1>Donation</h1> <div> <label for="amount">Amount:</label> <input type="text" name="amount" value="<?= $inputs['amount'] ?? '' ?>" id="amount" placeholder="Minimum donation $<?= MIN_DONATION ?>"> <small><?= $errors['amount'] ?? '' ?></small> </div> <button type="submit">Donate</button> </form> </main> </body> </html>
Code language: PHP (php)

How it works.

First, call the session_start() to access the $_SESSION data:

session_start();
Code language: PHP (php)

Second, sanitize and validate the amount like the previous example; Also, add the variables $valid, $errors, and $inputs to the session to be able to access them in the subsequent request.

// add variables to session $_SESSION['valid'] = $valid; $_SESSION['errors'] = $errors; $_SESSION['inputs'] = $inputs;
Code language: PHP (php)

Third, redirect the browser to the same page with the HTTP status code 303 using the header() function:

// redirect to the page itself header('Location: index.php', true, 303); exit;
Code language: PHP (php)

Fourth, get the variables from the session if the HTTP request method is the GET and remove them from the session immediately:

if (isset($_SESSION['valid'])) { $valid = $_SESSION['valid']; unset($_SESSION['valid']); } if (isset($_SESSION['errors'])) { $errors = $_SESSION['errors']; unset($_SESSION['errors']); } if (isset($_SESSION['inputs'])) { $errors = $_SESSION['inputs']; unset($_SESSION['inputs']); }
Code language: PHP (php)

The remaining code is the same as the previous example.

If you click the Refresh button after submitting the form, the browser won’t submit the form again. Instead, the browser loads the form using the GET method.

Summary

  • PRG stands for Post-Redirect-Get.
  • PRG helps prevent the double form submit problem.
Did you find this tutorial useful?