PHP 实战项目

学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——留言板、REST API、文件上传管理,分别覆盖 PHP 三大主力场景:Web 表单 + 数据库、接口服务、文件处理。每个项目都从需求出发,给出完整代码、运行结果和要点讲解。建议你亲手跑一遍(php -S localhost:8000 起内置服务器),再改改需求,这才是真正"上手"。

实战 1:留言板(PDO + CRUD + XSS 防护)

需求:做一个留言板——访客填昵称和内容提交,所有留言按时间倒序展示。数据用 SQLite 持久化(零配置),必须防住 SQL 注入和 XSS 攻击。

涉及知识:PDO 连接与预处理语句、SQLite 建表、$_POST 表单接收、htmlspecialchars 输出转义、PRG(Post-Redirect-Get)防重复提交。无需第三方依赖。

<?php
// guestbook.php — 留言板:展示留言 + 提交新留言
// 用 SQLite 存储(零配置),PDO 预处理防 SQL 注入,htmlspecialchars 防 XSS

// 1. 连接数据库(SQLite 单文件);建表(若不存在)
$pdo = new PDO('sqlite:guestbook.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    content TEXT NOT NULL,
    created TEXT DEFAULT CURRENT_TIMESTAMP
)");

// 2. 处理 POST 提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $content = trim($_POST['content'] ?? '');
    if ($name !== '' && $content !== '') {
        // 预处理语句:参数化绑定,彻底防 SQL 注入
        $stmt = $pdo->prepare(
            'INSERT INTO messages (name, content) VALUES (:name, :content)'
        );
        $stmt->execute([':name' => $name, ':content' => $content]);
        // PRG 模式:POST 后重定向到 GET,避免刷新重复提交
        header('Location: guestbook.php');
        exit;
    }
    $error = '昵称和内容都不能为空';
}

// 3. 读取全部留言(按时间倒序)
$rows = $pdo->query('SELECT * FROM messages ORDER BY id DESC')
            ->fetchAll(PDO::FETCH_ASSOC);

// e() 是 htmlspecialchars 的简写:输出到 HTML 时转义,防 XSS
function e(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html><meta charset="utf-8">
<h2>留言板</h2>
<?php if (!empty($error)): ?>
  <p style="color:red"><?= e($error) ?></p>
<?php endif; ?>
<form method="post">
  <input name="name" placeholder="昵称" required>
  <textarea name="content" placeholder="说点什么" required></textarea>
  <button type="submit">发表</button>
</form>
<?php foreach ($rows as $row): ?>
  <div>
    <strong><?= e($row['name']) ?></strong>
    <small><?= e($row['created']) ?></small>
    <p><?= nl2br(e($row['content'])) ?></p>
  </div>
<?php endforeach; ?>
<!-- 提交后页面会多出一条留言;XSS 测试:输入 <script>alert(1)</script> 会被原样转义成文本 -->

要点讲解:

实战 2:REST API(header json + 路由 + $_GET/$_POST)

需求:不用任何框架,纯 PHP 写一个商品 REST API——GET /products 列全部、GET /products?id=3 取单条、POST /products 新增。所有响应都是 JSON。

涉及知识:header('Content-Type: application/json')、$_SERVER['PATH_INFO'] 路由分发、php://input 读 body、json_encode/decode、HTTP 状态码。

<?php
// api.php — 极简 REST API:基于 PATH_INFO 路由分发 + JSON 响应
// 用法:GET /api.php/products       列出全部
//      GET /api.php/products?id=3  取单条
//      POST /api.php/products      新增(body 放 JSON)

header('Content-Type: application/json; charset=utf-8');

// 内存数据(真实项目从数据库取)
$products = [
    ['id' => 1, 'name' => '苹果', 'price' => 5.0],
    ['id' => 2, 'name' => '香蕉', 'price' => 3.0],
    ['id' => 3, 'name' => '橙子', 'price' => 4.0],
];

// 取 PATH_INFO 作为路由:/api.php/products → 'products'
$path = $_SERVER['PATH_INFO'] ?? '/';
$method = $_SERVER['REQUEST_METHOD'];

// json_out 统一返回 JSON
function json_out($data, int $code = 200): void {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

// 路由分发
if ($path === '/products') {
    if ($method === 'GET') {
        // 取单条 ?id=3
        if (isset($_GET['id'])) {
            $id = (int)$_GET['id'];
            foreach ($products as $p) {
                if ($p['id'] === $id) json_out($p);
            }
            json_out(['error' => "未找到 id={$id}"], 404);  // 双引号里 {$id} 自动解析
        }
        // 否则列全部
        json_out($products);
    }
    if ($method === 'POST') {
        // 从 body 读 JSON
        $body = json_decode(file_get_contents('php://input'), true);
        if (!$body || empty($body['name'])) {
            json_out(['error' => 'name 必填'], 400);
        }
        $new = [
            'id' => count($products) + 1,
            'name' => $body['name'],
            'price' => (float)($body['price'] ?? 0),
        ];
        json_out($new, 201);
    }
    json_out(['error' => '不支持的方法'], 405);
}

// 兜底:未匹配的路由
json_out(['error' => '路由不存在'], 404);

// 测试(另开终端):
// $ curl 'http://localhost:8000/api.php/products'
// [{"id":1,"name":"苹果","price":5},{"id":2,"name":"香蕉","price":3},{"id":3,"name":"橙子","price":4}]
// $ curl 'http://localhost:8000/api.php/products?id=2'
// {"id":2,"name":"香蕉","price":3}
// $ curl -X POST http://localhost:8000/api.php/products \
//        -H 'Content-Type: application/json' -d '{"name":"葡萄","price":8}'
// {"id":4,"name":"葡萄","price":8}

要点讲解:

实战 3:文件上传管理($_FILES + 验证 + 安全)

需求:做一个文件上传页面——用户选文件提交,服务端做类型检查、大小限制、随机文件名,安全地存到 uploads/ 目录。允许 JPG/PNG/GIF/PDF,最大 2 MB。

涉及知识:$_FILES 超全局数组、enctype="multipart/form-data" 表单、finfo 探测真实 MIME、move_uploaded_file、随机文件名防覆盖。

<?php
// upload.php — 文件上传:限制类型/大小、生成随机文件名、移到上传目录
// php.ini 配置:file_uploads=On, upload_max_filesize=2M, post_max_size=8M

const UPLOAD_DIR = __DIR__ . '/uploads';
const MAX_SIZE   = 2 * 1024 * 1024;            // 2 MB
const ALLOWED    = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];

// 确保目录存在
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);

function e(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file'])) {
    $f = $_FILES['file'];

    // 1. 检查错误码($_FILES['file']['error'])
    if ($f['error'] !== UPLOAD_ERR_OK) {
        $message = "上传错误码: {$f['error']}(常见:1=超 php.ini 限制,4=未选文件)";
    }
    // 2. 检查大小
    elseif ($f['size'] > MAX_SIZE) {
        $message = '文件太大,限制 2 MB 以内';
    }
    // 3. 检查真实 MIME(用 finfo 探测文件头,不信任 $_FILES['type'])
    else {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $f['tmp_name']);
        finfo_close($finfo);
        if (!in_array($mime, ALLOWED, true)) {
            $message = "不允许的类型: {$mime}";
        } else {
            // 4. 生成随机文件名,避免覆盖与路径遍历攻击
            $ext = pathinfo($f['name'], PATHINFO_EXTENSION);
            $safeName = bin2hex(random_bytes(8)) . '.' . $ext;
            $dest = UPLOAD_DIR . DIRECTORY_SEPARATOR . $safeName;
            // 5. is_uploaded_file + move_uploaded_file:只处理真上传的文件
            if (move_uploaded_file($f['tmp_name'], $dest)) {
                $message = "上传成功: {$safeName}";
            } else {
                $message = '移动文件失败';
            }
        }
    }
}
?>
<!DOCTYPE html><meta charset="utf-8">
<h2>文件上传</h2>
<?php if ($message): ?><p><?= e($message) ?></p><?php endif; ?>
<!-- enctype="multipart/form-data" 是上传表单必须的 -->
<form method="post" enctype="multipart/form-data">
  <input type="file" name="file" required>
  <button type="submit">上传</button>
</form>
<p>允许:JPG / PNG / GIF / PDF,最大 2 MB</p>

要点讲解:

小结

三个项目分别对应 PHP 三大主力场景:表单 + 数据库(留言板)、API 服务(REST)、文件处理(上传)。它们都反复强调了 PHP Web 开发的两条生命线:预处理语句防 SQL 注入、htmlspecialchars 防 XSS——这两条做扎实,你的站已经挡掉 80% 的攻击。建议至少把留言板改造成自己的版本(加分页、加管理员审核、加头像上传),做过一遍比读十遍更扎实。

恭喜!到这里 PHP 系列就真正完结了。接下来挑一个方向深入——大型框架(Laravel / Symfony)、API 服务(Slim + Doctrine)、或 WordPress 二次开发——边做边学,进步最快。

← 上一篇 PHP 练习题

返回 PHP 教程目录

✈️💬