How to connect a custom HTML form to a MySQL database using PHP
Most dynamic websites start with a simple idea: a visitor fills out a form, and your server saves that information. In this lesson you will build that pipeline end to end—HTML form, PHP script, and MySQL table—using modern PDO prepared statements so user input never becomes raw SQL text.
What you need before starting
Install a local environment with PHP 8+ and MySQL (MAMP, XAMPP, or Docker). Create an empty database called globaltuts_demo
and note your database username and password. You will also create one table to store newsletter signups as a harmless example.
Step 1 — Create the MySQL table
Run the following SQL in phpMyAdmin or the MySQL client. The table uses an auto-increment primary key and limits email length, which is a small data-integrity choice that prevents oversized values.
CREATE TABLE newsletter_signups (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
full_name VARCHAR(120) NOT NULL,
email VARCHAR(190) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Below is how the output typically looks in a browser, terminal, or API client:
| id | name |
|---|---|
| 1 | Sample row |
| 2 | Another row |
After execution, confirm the table exists with SHOW TABLES;. Keeping charset utf8mb4 ensures international names store correctly.
Step 2 — Database connection file
Separate database credentials into config.php so you can reuse the connection across scripts.
Never commit real passwords to public repositories; use environment variables on production servers.
<?php
declare(strict_types=1);
$dbHost = '127.0.0.1';
$dbName = 'globaltuts_demo';
$dbUser = 'root';
$dbPass = 'your_local_password';
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
try {
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('Database connection failed.');
}Below is how the output typically looks in a browser, terminal, or API client:
Database connection failed.
ERRMODE_EXCEPTION makes database errors throw exceptions you can log, instead of failing silently.
The generic public message avoids leaking connection details to attackers.
Step 3 — HTML form markup
The form uses method="post" so data is not visible in the query string. The action attribute points to your PHP handler.
Each input has a name attribute; those names become keys in $_POST.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Newsletter signup</title>
</head>
<body>
<h1>Join our learning list</h1>
<form action="save_signup.php" method="post">
<label>Full name
<input type="text" name="full_name" required maxlength="120">
</label>
<label>Email
<input type="email" name="email" required maxlength="190">
</label>
<button type="submit">Sign up</button>
</form>
</body>
</html>Below is how the output typically looks in a browser, terminal, or API client:
Step 4 — PHP handler with prepared INSERT
When the form submits, PHP receives $_POST['full_name'] and $_POST['email'].
Trim whitespace, validate the email with filter_var, then bind values to placeholders :name and :email.
Placeholders ensure the database driver treats input as data, not executable SQL.
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method not allowed.');
}
$name = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? '');
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(422);
exit('Invalid input.');
}
$sql = 'INSERT INTO newsletter_signups (full_name, email) VALUES (:name, :email)';
$stmt = $pdo->prepare($sql);
$stmt->execute(['name' => $name, 'email' => $email]);
header('Location: thank-you.html');
exit;Below is how the output typically looks in a browser, terminal, or API client:
HTTP 302 Redirect Location: thank-you.html (After a valid form submit)
The redirect after success implements the Post/Redirect/Get pattern, which stops accidental duplicate submissions when the user refreshes. In Part 3 you will expand validation and inline error display instead of a plain exit message.