PHP and MySQL have powered the web since the mid-1990s and they still do — about three quarters of all websites with a known server-side language run on PHP. The combination is forgiving for beginners, easy to deploy on cheap shared hosting, and surprisingly performant when you know what you are doing. This article walks through the modern, safe way to use them together: PDO, prepared statements, and patterns that will keep your application secure by default.

Why PHP and MySQL still matter

Every "PHP is dead" headline has been wrong for at least fifteen years. WordPress, Laravel, Symfony, Drupal, Magento — the largest CMSs and frameworks on the web all run on PHP. The language has modernised dramatically since PHP 7: strict types, null safety, a real package manager (Composer), and a vibrant ecosystem. MySQL is the database that almost everyone already knows how to operate.

You do not need PHP or MySQL for your next project — but if you are maintaining an existing site, learning them is unavoidable. This article gets you productive in one sitting.

Setting up your environment

Install PHP and MySQL locally. The easiest path is to download a stack that includes both:

  • macOS: MAMP or the Homebrew packages php and mysql.
  • Windows: XAMPP or Laragon.
  • Linux: sudo apt install php mysql-server php-mysql.

Verify the install:

php --version
mysql --version

You should see PHP 8.x or later. Older versions are still common on production servers but you should not write new code for them.

Your first PHP script

Save this as hello.php:

<?php
$greeting = "Hello, world!";
echo "<h1>$greeting</h1>";
?>

Open it in your browser via your local server. PHP files run on the server; the browser only sees the resulting HTML. Variables start with $. Strings can interpolate variables when wrapped in double quotes (but not single quotes).

Connecting to MySQL with PDO

PDO stands for PHP Data Objects. It is the modern, database-agnostic way to talk to MySQL (and SQLite, PostgreSQL, etc.) from PHP. Always use PDO — never the old mysql_* functions, which were removed in PHP 7.

<?php
$dsn = "mysql:host=localhost;dbname=blog;charset=utf8mb4";
$pdo = new PDO($dsn, "root", "secret");

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
?>

Setting the error mode to exceptions means PDO throws on errors instead of failing silently. You will want this. Catch exceptions in your top-level handler and log them.

Prepared statements: how to never get SQL-injected

SQL injection is the most common web vulnerability. It is also one of the easiest to prevent: always use prepared statements. Never concatenate user input into a SQL string.

The wrong way:

// DO NOT DO THIS
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";

That allows an attacker to type ' OR 1=1 -- as their email and dump your whole users table.

The right way:

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(["email" => $_POST['email']]);
$user = $stmt->fetch();

The :email is a placeholder. PDO sends the SQL and the value separately to MySQL, which knows they are different things and treats the value as data, not code. This pattern works for every query — SELECT, INSERT, UPDATE, DELETE.

Inserting data safely

$stmt = $pdo->prepare(
  "INSERT INTO articles (title, body, author_id) VALUES (:title, :body, :author_id)"
);
$stmt->execute([
  "title"    => $_POST["title"],
  "body"     => $_POST["body"],
  "author_id" => $userId,
]);
$newId = $pdo->lastInsertId();

Prepared statements let the database reuse the compiled query plan across many executions. They are also faster than escaping strings manually.

Reading data with fetch and fetchAll

$stmt = $pdo->prepare("SELECT id, title FROM articles WHERE published = 1");
$stmt->execute();
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($articles as $article) {
    echo "<h2>" . htmlspecialchars($article["title"]) . "</h2>";
}

FETCH_ASSOC returns each row as an associative array. htmlspecialchars escapes characters that have special meaning in HTML, preventing another common vulnerability: XSS (cross-site scripting). Always escape on output, never trust user input.

A small CRUD application

Let us build a tiny contacts list. The schema:

CREATE TABLE contacts (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(200) NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

The list page (index.php):

<?php
require "db.php";

$contacts = $pdo->query("SELECT * FROM contacts ORDER BY created_at DESC")->fetchAll();
?>
<!doctype html>
<title>Contacts</title>
<h1>Contacts</h1>
<ul>
<?php foreach ($contacts as $c): ?>
  <li><?= htmlspecialchars($c["name"]) ?> — <?= htmlspecialchars($c["email"]) ?></li>
<?php endforeach ?>
</ul>

The form handler (add.php):

<?php
require "db.php";

$stmt = $pdo->prepare("INSERT INTO contacts (name, email) VALUES (:n, :e)");
$stmt->execute([
  "n" => $_POST["name"],
  "e" => $_POST["email"],
]);
header("Location: /index.php");

Notice: htmlspecialchars on every output, prepared statements on every input. Two habits that prevent most of the security disasters you read about.

Hashing passwords

Never store passwords as plain text or with a simple SHA hash. Use PHP's built-in password_hash:

$hash = password_hash($_POST["password"], PASSWORD_BCRYPT);
// store $hash in the database

// verify later
if (password_verify($_POST["password"], $storedHash)) {
    // logged in
}

BCrypt is intentionally slow, which is a feature: it makes brute force attacks expensive. PHP also has password_needs_rehash for upgrading hashes when you change algorithms. See our Password Hashing Done Right article for a deeper dive.

Common pitfalls

  • Concatenating SQL strings. We have said this three times because it matters that much. Use prepared statements, always.
  • Not escaping output. Always run user-supplied data through htmlspecialchars when outputting HTML.
  • Displaying raw errors in production. error_reporting(E_ALL) in development, but in production log errors and show generic messages. Stack traces leak information.
  • Storing passwords in plain text. Use password_hash. Rotate any leaked credentials immediately.
  • Trusting client-side validation. Validate on the server, every time, even if you have already validated in JavaScript.

A more advanced thing: Composer and PSR-4

Composer is PHP's package manager. Every modern PHP project uses it. Initialise one with composer init, then install libraries with composer require vendor/package. The PSR-4 autoloading standard means you can write classes in namespaces without manually require-ing files. If you plan to write more than a hundred lines of PHP, learn Composer first. It is the difference between "script" and "application."

Working with forms and $_POST

PHP receives form data in two superglobals. $_GET holds URL parameters, $_POST holds form fields. Accessing them is trivial:

$email = $_POST["email"] ?? "";
$name = $_POST["name"] ?? "";

The ?? operator (null coalescing) returns the right-hand side if the left is null or undefined. Use it everywhere user input is read, otherwise you will see "Undefined index" notices.

Files uploaded via <input type="file"> end up in $_FILES. Always validate that the file actually arrived and check its MIME type. The user can send anything they want.

Routing PHP requests

Modern PHP apps use a single front controller: every request goes through index.php, which decides what to do based on the URL. Apache and Nginx rewrite non-existent paths to the front controller:

# Apache .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQ<h2>Further reading</h2>
<p>PHP and MySQL have been around long enough that there is a great deal of bad advice on the internet. The three sources below are the ones we trust — the official PHP manual, the PDO reference, and OWASP’s guidance on preventing SQL injection.</p>
<ul>
<li><strong><a href="https://www.php.net/docs.php" target="_blank" rel="noopener">PHP official documentation</a target="_blank" rel="noopener noreferrer"></strong> — the official PHP manual, including the language reference, every built-in function, and the security chapter.</li>
<li><strong><a href="https://www.php.net/manual/en/book.pdo.php" target="_blank" rel="noopener">PDO manual</a target="_blank" rel="noopener noreferrer"></strong> — the official documentation for PHP Data Objects, the recommended way to access databases in modern PHP.</li>
<li><strong><a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html" target="_blank" rel="noopener">OWASP SQL injection prevention cheatsheet</a target="_blank" rel="noopener noreferrer"></strong> — OWASP’s canonical guide to preventing SQL injection, with language-specific recommendations for PHP.</li>
</ul>

UEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Then in PHP:

$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($path === "/")         require "pages/home.php";
elseif ($path === "/about") require "pages/about.php";

Frameworks like Laravel add a proper router on top of this pattern. For a small app, the if-elseif chain is fine.

Namespaces and the modern PHP ecosystem

PHP 5.3 introduced namespaces, which let you group related classes under a common prefix. The PSR-4 autoloading standard maps namespaces to folder paths: App\Controllers\ArticleController lives in src/Controllers/ArticleController.php. With Composer and an autoloader, you never have to require a file manually again.

Modern PHP code uses strict types: declare declare(strict_types=1); at the top of every file. It catches type errors at the point of the call rather than producing surprising coercion (the famous "1" + 1 == 2).

Further reading

PHP and MySQL have been around long enough that there is a lot of outdated advice on the web. These are the sources that are still correct in 2026.

FAQ

PHP or Node.js?

Both are fine. PHP is simpler to deploy on cheap hosting and has more CMSs. Node.js is faster for highly concurrent workloads and shares a language with the browser. Pick PHP if your existing project uses it or you need a CMS. Pick Node.js if you are starting fresh and your team prefers JavaScript.

How do I deploy PHP?

Almost any web host supports PHP out of the box: shared hosting, VPS, Docker. Upload your files, point Apache or Nginx at the public/ directory, done. For containerised deployments, the official php Docker image plus nginx is the standard recipe.

MySQL or MariaDB?

For almost every practical purpose they are the same. MariaDB is a community fork of MySQL with extra features. Most PHP code runs unchanged on either. Pick whichever your host offers.

What about Laravel?

Laravel is the dominant PHP framework. It provides routing, ORM (Eloquent), templating (Blade), authentication, queues, and a million other things. If you are building a real application, use Laravel or Symfony instead of raw PHP. This article teaches raw PHP because you need to understand the foundation before the framework.

How do I prevent XSS?

Escape on output. Every variable you put inside HTML goes through htmlspecialchars. Most templating engines do this automatically; in raw PHP you have to remember.

How do I handle sessions?

Call session_start() at the top of any page that uses sessions, then $_SESSION["user_id"] = $id to set values. Sessions are stored on the server and identified by a cookie on the client. Never trust session IDs from the client — regenerate the ID on login to prevent session fixation.

How do I handle multiple database connections?

Open separate connections per request in PDO, or use a connection pool library. For high-traffic apps, the connection overhead can dominate. Use persistent connections (pconnect) only if you fully understand the lifecycle implications — they are not always faster.

Take it slow and read every error message carefully.

Homework

Build a small contact-form app:

  • A form that captures name, email, and a message.
  • Server-side validation for each field.
  • Save submissions to MySQL using prepared statements.
  • Display a list of all submissions on a separate page, with HTML escaping.
  • Add a "delete" action with a confirmation step.
  • Hash a "password" field with password_hash as a learning exercise.

Bonus: use Composer to install vlucas/valitron (a small validation library) and refactor your validation logic into reusable rules. Once that works, you have the foundation for any PHP project you will ever build.