
最近折腾了一下用 PHP + MySQL 搭一个带数据库的博客网站,踩了几个坑,顺手把过程捋了一遍。这篇讲清楚怎么从零开始,用 PDO(PHP Data Objects)连接数据库,做一个完整的增删改查(CRUD)小项目。
开干。
数据库驱动的网站把内容存在数据库里,而不是硬编码在 HTML 文件中。用户访问页面时,PHP 查询数据库,动态生成 HTML 返回。
静态站点:手动编辑 HTML 文件。
数据库驱动的站点:编辑数据库记录,PHP 自动构建页面。
博客、电商平台、社交媒体,还有 WordPress 这类 CMS(内容管理系统)都是这个套路。
先建一个数据库和一张表。打开 MySQL 终端或 phpMyAdmin,执行:
CREATE DATABASE IF NOT EXISTS blog_demo; USE blog_demo; CREATE TABLE posts ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, author VARCHAR(100) DEFAULT 'Anonymous', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这张 posts 表包含自增 ID、标题、正文、作者名和创建时间戳。
插入一些示例数据:
INSERT INTO posts (title, content, author) VALUES
('我的第一篇博客', '你好世界!这是一篇数据库驱动的文章。', '张三'),
('学习 PHP', 'PHP 让动态网站开发变得简单。', '张三'),
('MySQL 基础', 'MySQL 是一个强大的关系型数据库管理系统。', '李四');
新建一个文件叫 config.php:
<?php
$host = '127.0.0.1';
$dbname = 'blog_demo';
$username = 'root';
$password = ''; try { $pdo = new PDO( "mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password ); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); echo "✅ Connected successfully!";
} catch (PDOException $e) { die("❌ Connection failed: " . $mf . $e->getMessage());
}
几个关键点:
PDO(PHP Data Objects)支持多种数据库——MySQL、PostgreSQL、SQLite 等charset=utf8mb4 支持表情符号和特殊字符ERRMODE_EXCEPTION 遇错抛异常,方便调试FETCH_ASSOC 返回关联数组格式的结果新建 index.php——首页:
<?php require 'config.php'; ?> <!DOCTYPE html>
<html lang="en">
<head> <title>我的博客</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .post { border-bottom: 1px solid #eee; padding: 15px 0; } .post h2 { margin: 0 0 5px; } .post .meta { color: #666; font-size: 0.9em; } a { color: #3498db; text-decoration: none; } a:hover { text-decoration: underline; } </style>
</head>
<body> <h1>我的博客</h1> <a href="create.php">+ 新建文章</a> <?php $stmt = $pdo->query("SELECT * FROM posts ORDER BY created_at DESC"); $posts = $stmt->fetchAll(); foreach ($posts as $post): ?> <div class="post"> <h2> <a href="post.php?id=<?= $post['id'] ?>"> <?= htmlspecialchars($post['title']) ?> </a> </h2> <p><?= nl2br(htmlspecialchars(substr($post['content'], 0, 200))) ?>...</p> <div class="meta"> 作者:<?= htmlspecialchars($post['author']) ?> | <?= date('Y年n月j日', strtotime($post['created_at'])) ?> </div> </div> <?php endforeach; ?>
</body>
</html>
安全提示:输出数据库数据时一定要用 htmlspecialchars()——能防止 XSS(跨站脚本攻击)。
新建 create.php:
<?php require 'config.php'; ?> <!DOCTYPE html>
<html lang="en">
<head> <title>新建文章</title> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; } input, textarea { width: 100%; padding: 8px; margin: 5px 0 15px; border: 1px solid #ddd; border-radius: 4px; } button { background: #3498db; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px; } .success { background: #2ecc71; color: white; padding: 10px; border-radius: 4px; margin-bottom: 15px; } </style>
</head>
<body> <h1>创建新文章</h1> <a href="index.php">← 返回博客</a> <?php if ($_SERVER['REQUEST_METHOD'] === 'POST'): $title = $_POST['title'] ?? ''; $content = $_POST['content'] ?? ''; $author = $_POST['author'] ?? '匿名用户'; // 校验 $errors = []; if (empty(trim($title))) $errors[] = '标题不能为空'; if (empty(trim($content))) $errors[] = '内容不能为空'; if (empty($errors)) { // 使用预处理语句,防止 SQL 注入 $stmt = $pdo->prepare( "INSERT INTO posts (title, content, author) VALUES (:title, :content, :author)" ); $stmt->execute([ ':title' => htmlspecialchars($title), ':content' => htmlspecialchars($content), ':author' => htmlspecialchars($author), ]); echo '<div class="success">✅ 文章创建成功!</div>'; } else { foreach ($errors as $error) { echo "<p style='color:red'>❌ $error</p>"; } } endif; ?> <form method="POST"> <label>标题 *</label> <input type="text" name="title" required> <label>内容 *</label> <textarea name="content" rows="6" required></textarea> <label>作者</label> <input type="text" name="author" value="匿名用户"> <button type="submit">发布文章</button> </form>
</body>
</html>
为什么要用预处理语句?它把 SQL 逻辑和数据分开,让 SQL 注入无从下手。切记不要用字符串拼接的方式把用户输入直接塞进 SQL。
新建 post.php:
<?php require 'config.php'; ?> <!DOCTYPE html>
<html lang="en">
<head> <title>查看文章</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .meta { color: #666; font-size: 0.9em; margin-bottom: 20px; } a { color: #3498db; } </style>
</head>
<body> <a href="index.php">← 返回博客</a> <?php $id = $_GET['id'] ?? 0; // 预处理语句——防止 SQL 注入 $stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :id"); $stmt->execute([':id' => $id]); $post = $stmt->fetch(); if (!$post): ?> <h1>文章未找到</h1> <p>抱歉,这篇文章不存在。</p> <?php else: ?> <h1><?= htmlspecialchars($post['title']) ?></h1> <div class="meta"> 作者:<?= htmlspecialchars($post['author']) ?> | <?= date('Y年n月j日 G:i', strtotime($post['created_at'])) ?> </div> <div><?= nl2br(htmlspecialchars($post['content'])) ?></div> <p style="margin-top:20px"> <a href="edit.php?id=<?= $post['id'] ?>">✏️ 编辑</a> | <a href="delete.php?id=<?= $post['id'] ?>" onclick="return confirm('确定删除这篇文章?')">🗑️ 删除</a> </p> <?php endif; ?>
</body>
</html>
新建 edit.php:
<?php require 'config.php'; ?> <?php
$id = $_GET['id'] ?? 0; // 获取现有文章
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :id");
$stmt->execute([':id' => $id]);
$post = $stmt->fetch(); if (!$post) { die('文章未找到');
} if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = $pdo->prepare( "UPDATE posts SET title = :title, content = :content, author = :author WHERE id = :id" ); $stmt->execute([ ':title' => htmlspecialchars($_POST['title']), ':content' => htmlspecialchars($_POST['content']), ':author' => htmlspecialchars($_POST['author'] ?? '匿名用户'), ':id' => $id, ]); $success = '✅ 文章已更新!';
}
?> <!DOCTYPE html>
<html lang="en">
<head> <title>编辑文章</title> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; } input, textarea { width: 100%; padding: 8px; margin: 5px 0 15px; border: 1px solid #ddd; border-radius: 4px; } button { background: #27ae60; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 4px; } .success { background: #2ecc71; color: white; padding: 10px; border-radius: 4px; margin-bottom: 15px; } </style>
</head>
<body> <h1>编辑文章</h1> <a href="post.php?id=<?= $id ?>">← 查看文章</a> <?php if (isset($success)) echo "<div class='success'>$success</div>"; ?> <form method="POST"> <label>标题</label> <input type="text" name="title" value="<?= htmlspecialchars($post['title']) ?>" required> <label>内容</label> <textarea name="content" rows="6" required><?= htmlspecialchars($post['content']) ?></textarea> <label>作者</label> <input type="text" name="author" value="<?= htmlspecialchars($post['author']) ?>"> <button type="submit">更新文章</button> </form>
</body>
</html>
新建 delete.php:
<?php
require 'config.php'; $id = $_GET['id'] ?? 0; $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id");
$stmt->execute([':id' => $id]); header('Location: index.php');
exit;
项目结构:
blog-demo/
├── config.php # 数据库连接
├── index.php # 首页——列出所有文章
├── create.php # 新建文章
├── post.php # 查看单篇文章(含编辑/删除链接)
├── edit.php # 编辑文章
└── delete.php # 删除文章
用 PHP 内置服务器启动:
php -S localhost:8000
然后浏览器访问 http://localhost:8000 就能看到效果。
| 做法 | 原因 |
|---|---|
| 预处理语句 | 防止 SQL 注入 |
| htmlspecialchars() | 防止 XSS 攻击 |
| 输入校验 | 阻止空数据或畸形数据入库 |
| 不信任任何用户输入 | 所有数据都要过滤和清理 |
基础打好了,可以往这些方向扩展:
?id=1 改成 /my-post-title恭喜!一个完整的 PHP + MySQL CRUD 小应用就这样搭起来了。核心要点:
WordPress、Laravel 以及所有主流 PHP 应用都是这套模式。吃透这些基础,想做什么网站都不在话下。
开干吧!🚀