Dynamic websites never work without a form. In this post, we’ll teach you how to add one with PHP.
In the classic case, data submitted from a form is sent to a PHP script, which then handles the processing.
The first thing to do is prepare the form. We’ll assume that the HTML form you create is located in the form.php file. To test it, you can place it in the document root directory of the local web server. Processing should also take place in the same file. For this reason, the action attribute can be left completely empty, as shown in this code.
<!DOCTYPE html>
<html>
<head>
<title>Registration form</title>
</head>
<body>
<form action="" method="POST">
<fieldset>
<legend>Personal details</legend>
<label>
First name:
<input type="text" name="firstname" size="20" maxlength="50"/>
</label>
…
</fieldset>
<input type="submit" value="Submit form"/>
</form>
</body>
</html>
If you use MAMP for local development—such as I do—you can find the form file at the URL http://localhost:8888/form.php in your browser, as shown in this figure.
The next figure shows how the browser gets to the form. Although the form.php file is passed to the PHP interpreter, nothing is processed because no PHP code exists yet.
Once the form has been submitted, the browser will call the same file again. However, the data in the form is then additionally transferred using the POST method. Form data can be read by the predefined $_POST variable. At the same time, you can quickly check whether the form has been sent, as the next listing shows. You can also see how the PHP code was integrated into the original source code. This part can now be explicitly processed by PHP, as shown in the next figure.
<!DOCTYPE html>
<html>
<head>
<title>Registration form</title>
</head>
<body>
<?php
if ( ! empty( $_POST ) ) {
// Form data is available
}
?>
<form action="" method="POST">
…
</form>
</body>
</html>
The next step is to receive and verify the form data. Verification is performed for security reasons since you can never be sure whether a form contains malicious data. You should always assume that user input is insecure and therefore neutralize it.
As a rule, a distinction is made between three types of neutralization:
However, one cannot exist without the other. For example, you can’t include a @ character in a string through sanitization it the input lacks one. You can only remove data by means of sanitization, not add new data. Validation is not performed until a sanitization has been performed first. Escaping follows last and is performed with the output to the screen.
Let’s start with the sanitization step, shown in the code below. PHP provides an interesting function for this purpose: filter_input_array(). (For more information, see this page.) First, the INPUT_POST constant determines that the data was submitted using the POST method. In this way, the programming language knows that it has to search for the data within the predefined $_POST variable. It then takes an array whose name corresponds to the input names of the form data. A cleanup filter is specified as the value of the array in the form of a constant. (An overview of all available filters can be found here.) Thus, in this first step, we are removing all the characters we don’t want to accept.
<?php
if ( ! empty( $_POST ) ) {
$formFields = [
'firstname' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'lastname' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'email' => FILTER_SANITIZE_EMAIL,
'password' => FILTER_UNSAFE_RAW,
'browser' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'feedback' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'improvements' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'newsletter' => FILTER_SANITIZE_FULL_SPECIAL_CHARS
];
$formData = filter_input_array(
INPUT_POST,
$formFields
);
}
In the second step, we want to validate whether the contained data is valid at all, for example, whether the specified email address is actually a valid email address. Conveniently, the same function can also handle validation, as shown below. Multiple filters are connected via the | character. In our example, some filters have been dropped. For example, the value in newsletter does not need to be sanitized if only true or false can be returned later anyway. The values for password and browser get a callback function passed with the options attribute.
<?php
if ( ! empty( $_POST ) ) {
$formFields = [
'firstname' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'lastname' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'email' => FILTER_SANITIZE_EMAIL | FILTER_VALIDATE_EMAIL,
'password' => [
'filter' => FILTER_CALLBACK,
'flags' => FILTER_REQUIRE_SCALAR,
'options' => 'hashPassword'
],
'browser' => [
'filter' => FILTER_CALLBACK,
'flags' => FILTER_REQUIRE_SCALAR,
'options' => 'enumBrowsers'
],
'feedback' => FILTER_VALIDATE_BOOLEAN,
'improvements' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'newsletter' => FILTER_VALIDATE_BOOLEAN
];
$formData = filter_input_array(
INPUT_POST,
$formFields
);
}
The actual functions are shown in the code below. The password is not sanitized because it is encrypted by means of the crypt() function. The crypt() function requires a salt so that a stronger hash value can be supplied. A salt is nothing more than a complex string stored in the PASSWORD_SALT constant. Encryption does not take place until a password has been specified. An empty password would also be encrypted and thus result in a hash value, which is not intended in this case.
<?php
define(
'PASSWORD_SALT',
'wJMM4|{UT<&r<*~%.b:AomWvw21(B5Gc_m:uSk^f,4bWHHMw|Su>@?5F7D4g)>~H'
);
function enumBrowsers( $value ): ?string {
$possibleValues = [
'chrome',
'edge',
'firefox',
'opera',
'safari'
];
foreach ( $possibleValues as $possibleValue ) {
if ( $possibleValue === $value ) {
return $value;
}
}
return null;
}
function hashPassword( $password ): string {
return ! empty( $password ) ? crypt( $password, PASSWORD_SALT ) : '';
}
Finally, the code should be designed in such a way that it throws an error message if the important fields are not filled out. For this purpose, the required fields are first specified in the listing below and are then run through with a foreach loop. If an error occurs, the name of the field will be written to the $errorFields variable. count() is then used to check whether any errors were recorded in it. If yes, the error message will be output, as shown in the figure.
<?php
if ( ! empty( $_POST ) ) {
$formFields = [
…
];
$formData = filter_input_array(
INPUT_POST,
$formFields
);
$requiredFields = [
'firstname',
'lastname',
'email',
'password'
];
$errorFields = [];
foreach ( $requiredFields as $requiredField ) {
if ( empty( $formData[ $requiredField ] ) ) {
$errorFields[] = $requiredField;
}
}
if ( count( $errorFields ) > 0 ) {
echo '<p><strong>Please fill out the following fields: '
. implode( ', ', $errorFields )
. '!</strong></p>';
}
}
The now sanitized and validated data can be further processed, perhaps for example, writing it to a database. PHP provides vendor-specific database extensions for this purpose.
The cURL extension would immediately enable you to add the visitor to a newsletter list if the provider provides an Application Programming Interface (API) for that.
Many more things are possible. In addition, conveniently, a user can display the form data again in the form if an error occurred because nothing is more frustrating than a suddenly empty form. The next listing shows how the value attributes of these fields are filled in. Note that escaping can be omitted because the FILTER_SANITIZE_FULL_SPECIAL_CHARS filter already converts all corresponding special characters. The two question marks represent the null coalescing operator. This operator ensures that an empty string is returned if the variable does not exist, for instance, if the form was displayed and not submitted. The following figure shows how the form data is preserved when an error occurs. In this way, a visitor can fix the error and submit it again. The complete example can be downloaded for free on the product page for the Rheinwerk Computing book Full Stack Web Development: The Comprehensive Guide.
<form action="" method="POST">
<fieldset>
<legend>Personal details</legend>
<label>
First name:
<input type="text" name="firstname" value="<?php
echo $formData['firstname'] ?? '';
?>" size="20" maxlength="50"/>
</label>
<br/>
<label>
Last name:
<input type="text" name="lastname" value="<?php
echo $formData['lastname'] ?? '';
?>" size="30" maxlength="70"/>
</label>
<br/>
<label>
Email:
<input type="email" name="email" value="<?php
echo $formData['email'] ?? '';
?>" size="30" maxlength="70"/>
</label>
<br/>
…
As described earlier, form data can be processed in a variety of ways. At this point, we want to use the email dispatch via PHP function mail(), as shown in the figure below. (For more information, see this page.) The if is followed by an else block that performs the following operations:
<?php
...
if ( count( $errorFields ) > 0 ) {
echo '<p><strong>Please fill out the following fields: '
. implode( ', ', $errorFields )
. '!</strong></p>';
} else {
$sendData = $formData;
array_walk( $sendData, fn( &$val, $key ) => $val = $key . ': ' . $val );
$message = implode("\r\n", $sendData );
$mailSent = mail(
'info@philipackermann.de',
'Mail from registration form',
wordwrap( $message, 70, "\r\n" )
);
if ( ! $mailSent ) {
echo '<p><strong>Unfortunately, the email could'
.'not be sent.</strong></p>';
} else {
echo '<p><strong>Email has been sent!</strong></p>';
}
}
The figure below shows the data sent in the email.
Note that the mail() function is not suitable for sending a large number of emails in a loop. This function opens and closes an SMTP socket with every email, which hurts performance. Another problem arises when the function is blocked by the web host. In this case, you want to use a framework such as PHPMailer, to create your own SMTP connections. You can find more information about this project here.
Editor’s note: This post has been adapted from a section of the book Full Stack Web Development: The Comprehensive Guide by Philip Ackermann. Philip is the CTO of Cedalo GmbH and the author of several reference books and technical articles on Java, JavaScript, and web development. His focus is on the design and development of Node.js projects in the areas of Industry 4.0 and Internet of Things.
This post was originally published 4/2025.