Short description: This lesson teaches you how to use PHP's PDO (PHP Data Objects) to perform secure, maintainable CRUD (Create, Read, Update, Delete) operations with MySQL. It includes ready-to-use code examples, real-life business scenarios (e.g., ecommerce product management), best practices for security and performance, and patterns you can reuse across projects.
Use this schema for the examples in this lesson:
CREATE TABLE products ( product_id INT AUTO_INCREMENT PRIMARY KEY, product_name VARCHAR(255) NOT NULL, product_price INT NOT NULL, product_status TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); Always have a single, reusable function or class to create PDO connections. Set PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION so errors throw exceptions you can catch and handle.
<?php function getPDO(): PDO { $dsn = 'mysql:host=127.0.0.1;dbname=aiwephppdobasics_example_basic_ecommerce;charset=utf8mb4'; $user = 'root'; $password = ''; try { $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); return $pdo; } catch (PDOException $e) { // in production: log error and show friendly message die('Connection failed: ' . $e->getMessage()); } } ?> Use named parameters or positional parameters and bind values. Named parameters are often clearer in larger queries.
<?php $pdo = getPDO();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['pr_name'])) {
$product_name = trim($_POST['pr_name']);
$product_price = (int)($_POST['price'] ?? 0);
$product_status = 1;
$sql = "INSERT INTO products (product_name, product_price, product_status)
VALUES (:pr_name, :price, :pr_status)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':pr_name', $product_name, PDO::PARAM_STR);
$stmt->bindParam(':price', $product_price, PDO::PARAM_INT);
$stmt->bindParam(':pr_status', $product_status, PDO::PARAM_INT);
$stmt->execute();
$id = $pdo->lastInsertId();
// fetch row after insert (useful for APIs)
$select = $pdo->prepare("SELECT * FROM products WHERE product_id = :id");
$select->execute([':id' => $id]);
$newProduct = $select->fetch();
echo '<pre>'; print_r($newProduct); echo '</pre>';
}
?>
Notes: Use lastInsertId() carefully with multi-master DBs; for typical single MySQL server it’s fine. Validate and sanitize inputs server-side before inserting.
Simple select (multiple rows):
<?php $pdo = getPDO(); $minPrice = 10; $sql = "SELECT * FROM products WHERE product_price > :minPrice AND product_status = :status"; $stmt = $pdo->prepare($sql); $stmt->execute([':minPrice' => $minPrice, ':status' => 1]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); echo '<pre>'; print_r([$stmt->rowCount(), $rows]); echo '</pre>'; ?> Selecting a single row:
<?php $id = 2; $stmt = $pdo->prepare("SELECT * FROM products WHERE product_id = ?"); $stmt->execute([$id]); $product = $stmt->fetch(PDO::FETCH_ASSOC); if ($product) { // found } else { // not found } ?> Best practice: Use named parameters for clarity in complex queries. Use appropriate fetch mode to reduce memory usage.
Bind parameters to avoid injections and to let PDO handle quoting.
<?php $pdo = getPDO(); $new_name = 'iPhone x1811'; $id = 2;
$update_sql = "UPDATE products SET product_name = :product_name WHERE product_id = :id";
$stmt = $pdo->prepare($update_sql);
$stmt->bindParam(':product_name', $new_name, PDO::PARAM_STR);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
echo 'Rows updated: ' . $stmt->rowCount();
?>
Notes: rowCount() returns the number of affected rows. When updating multiple fields, only set the fields that changed.
<?php $pdo = getPDO(); $idToDelete = 13; $stmt = $pdo->prepare("DELETE FROM products WHERE product_id = ?"); $stmt->execute([$idToDelete]); echo 'Deleted rows: ' . $stmt->rowCount(); ?> Soft delete alternative: For business apps prefer a soft-delete pattern (deleted_at TIMESTAMP NULL or product_status) so you can restore items and keep historical data for audits.
Wrap related operations in a transaction so the DB either commits all changes or none.
<?php $pdo = getPDO(); try { $pdo->beginTransaction();
// Example: create an order and reduce inventory
// insert order
// update product stock
// insert order items
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
// log error, show friendly message
echo 'Operation failed: ' . $e->getMessage();
}
?>
product_price, product_status) for WHERE clauses used frequently.EXPLAIN in MySQL for expensive queries to identify bottlenecks.Simple pagination with prepared statements:
<?php $pdo = getPDO();
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 20;
$offset = ($page - 1) * $perPage;
$sql = "SELECT * FROM products WHERE product_status = :status ORDER BY created_at DESC LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
// Note: LIMIT and OFFSET need integer binding via bindValue with explicit type
$stmt->bindValue(':status', 1, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
?>
Search: use fulltext indexes or sanitized LIKE patterns for basic search. For large catalogs, consider a dedicated search engine (Elasticsearch, MeiliSearch).
A small ecommerce admin panel where staff can add, edit, list, and soft-delete products. Use transactions when creating product + images + inventory rows. Validate price & stock before commit. Show friendly messages and log errors for troubleshooting.
When a customer places an order you must:
When admin updates a product, write a change log entry (who, when, what changed). Logs are invaluable for audit and debugging — keep them in a separate table or service.
rowCount() is not guaranteed to return number of rows for SELECT on all drivers — use count($result) or fetchAll carefully.PDO::PARAM_INT via bindValue.Use development and production configuration differences:
schema.org markup for tutorials/articles if your CMS supports it.insert_pdo.php (form + insert + post-insert select):
<?php // insert_pdo.php $pdo = getPDO();
$product_name = $_POST['pr_name'] ?? '';
$product_price = $_POST['price'] ?? 0;
$product_status = 1;
if (!empty($product_name)) {
$sql = "INSERT INTO products (product_name, product_price, product_status) VALUES (:pr_name, :price, :pr_status)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':pr_name', $product_name, PDO::PARAM_STR);
$stmt->bindParam(':price', $product_price, PDO::PARAM_INT);
$stmt->bindParam(':pr_status', $product_status, PDO::PARAM_INT);
$stmt->execute();
$id = $pdo->lastInsertId();
$q = $pdo->prepare("SELECT * FROM products WHERE product_id = :id");
$q->bindParam(':id', $id, PDO::PARAM_INT);
$q->execute();
$fetch = $q->fetch(PDO::FETCH_ASSOC);
echo '<pre>'; print_r($fetch); echo '</pre>';
}
?>
select_pdo.php (select by price):
<?php // select_pdo.php $pdo = getPDO(); $price = 10; $stmt = $pdo->prepare("SELECT * FROM products WHERE product_price > ? AND product_status = ?"); $stmt->execute([$price, 1]); $rows = $stmt->fetchAll(PDO::FETCH_NUM); echo '<pre>'; print_r([count($rows), $rows]); echo '</pre>'; ?> update_pdo.php (update name):
<?php // update_pdo.php $pdo = getPDO(); $new_name = 'iPhone x1811'; $id = 2; $update = $pdo->prepare("UPDATE products SET product_name = :product_name WHERE product_id = :id"); $update->execute([':product_name' => $new_name, ':id' => $id]); var_dump($update->rowCount()); ?> delete_pdo.php:
<?php // delete_pdo.php $pdo = getPDO(); $stmt = $pdo->prepare("DELETE FROM products WHERE product_id = ?"); $stmt->execute([13]); echo 'Deleted: ' . $stmt->rowCount(); ?> Using PDO for CRUD in PHP is a major step toward building secure, maintainable applications. Start by extracting DB logic into functions or a small data layer (DAO or Repository pattern). Then add validation, transactions, and logging. For larger systems, consider ORM (Doctrine, Eloquent) only after understanding raw PDO operations — raw PDO knowledge helps you debug and optimize queries when ORMs leak inefficiencies.
Suggested exercises for this lesson:
SELECT ... FOR UPDATE).