兄弟连PHP教程:从基础语法到项目实战

兄弟连PHP教程:从基础语法到项目实战

兄弟连PHP教程:从基础语法到项目实战

PHP 是一种主要运行在服务器端的脚本语言,常用于生成网页、处理表单、提供 HTTP 接口,以及连接数据库。它上手快,部署选择多,适合内容管理系统、中小型网站、内部工具和已有 PHP 系统的维护。PHP 不是浏览器端语言,也不应被当作 JavaScript 的替代品:页面交互通常由 JavaScript 处理,PHP 负责服务器端的业务逻辑。

本文以 PHP 8.x 为基础,从语法和请求处理讲到一个可运行的待办清单项目。示例重点放在实际开发习惯上:区分数据类型、验证用户输入、使用预处理语句访问数据库,并对输出进行转义。文中的本地服务器仅供开发测试,不等同于生产环境部署方案。

一、安装与运行方式

先在终端检查 PHP 是否可用:

php -v

如果电脑尚未安装 PHP,可使用操作系统的软件包管理器,或使用适合本机系统的 PHP 集成环境。需要数据库功能时,还要确认对应扩展已启用。本文项目使用 SQLite,要求 PHP 安装并启用 PDO 和 pdo_sqlite 扩展,可用以下命令查看:

php -m

PHP 文件通常以 .php 结尾。命令行脚本可以直接执行;网页项目则需要 Web 服务器将 HTTP 请求交给 PHP 处理。开发阶段可在项目目录运行 PHP 自带的开发服务器:

php -S 127.0.0.1:8000

随后在浏览器访问 http://127.0.0.1:8000。这个服务器方便本地测试,但不是面向生产环境的高性能、完整 Web 服务器。正式部署时应配置适当的 Web 服务器、PHP 运行方式、HTTPS、日志和权限。

二、PHP 基础语法

1. PHP 标签、语句与注释

PHP 代码通常写在 <?php 与 ?> 之间。每条语句通常以分号结束。纯 PHP 文件末尾可以省略结束标签,避免标签之后意外输出空格,影响响应头处理。

<?php
// 单行注释
/*
多行注释
*/
echo "你好,PHP!";

echo 用于输出内容,常见于生成 HTML。它不会自动对 HTML 特殊字符进行转义,因此不能把未经处理的用户输入直接拼接到页面里。

2. 变量与类型

PHP 变量以美元符号 $ 开头,变量名区分大小写。PHP 是动态类型语言,变量类型会随赋值改变,但并不意味着可以忽略类型和输入校验。

<?php
$name = "小林";       // 字符串
$count = 3;           // 整数
$price = 19.9;        // 浮点数
$isAvailable = true;  // 布尔值
$items = ["书", "笔"]; // 数组

echo $name;
echo $items[0];

数组既可用数字索引,也可用字符串键。访问外部输入时应先检查键是否存在;直接读取不存在的键可能产生警告,也可能让程序进入非预期分支。

<?php
$user = [
    "name" => "小林",
    "role" => "reader",
];

echo $user["name"];

if (isset($user["role"])) {
    echo $user["role"];
}

isset() 检查变量或数组键是否存在且不为 null;若要区分“不存在”和“值为 null”,可使用 array_key_exists()。对请求参数,更常见的做法是使用 ?? 提供默认值:

<?php
$page = $_GET["page"] ?? "1";

3. 比较、条件和循环

比较值时优先使用严格比较符 === 和 !==。宽松比较会尝试转换类型,在处理来自表单、URL 或 JSON 的数据时,可能产生令人意外的结果。

<?php
$age = 20;

if ($age >= 18) {
    echo "已成年";
} else {
    echo "未成年";
}

$role = "admin";
if ($role === "admin") {
    echo "允许进入管理区域";
}

foreach 常用于遍历数组;for 适合重复次数明确的场景,while 则会在条件成立时持续执行。循环条件必须能够结束,否则脚本会一直运行,直至受到执行时间或资源限制。

<?php
$colors = ["红", "绿", "蓝"];

foreach ($colors as $color) {
    echo $color . "\n";
}

4. 函数与作用域

函数用于封装可复用逻辑。参数类型和返回类型能够帮助代码明确约定;类型声明并不会替代业务校验,例如“字符串”不一定符合长度或格式要求。

<?php
function makeGreeting(string $name): string
{
    return "你好," . $name;
}

echo makeGreeting("小林");

函数内部定义的变量通常是局部变量。与其依赖全局变量,不如通过参数传入依赖,通过返回值传出结果,让函数更容易测试和维护。

5. 字符串与格式化

点号 . 用于连接字符串。双引号字符串可以解析其中的变量,单引号字符串则更适合不需要变量插值的文本。

<?php
$name = "小林";
echo "你好,{$name}";
echo '这是一段固定文本';

字符串拼接只负责组合文本,不会自动处理字符编码、HTML 转义或 SQL 安全。不同输出场景需要采用不同处理方式:HTML 页面使用 HTML 转义,数据库查询使用预处理语句,命令行参数则应使用适合该命令环境的安全接口。

三、从 HTTP 请求接收数据

常见的超全局变量包括 $_GET、$_POST、$_SERVER 和 $_COOKIE。它们由 PHP 提供,能够在函数中直接访问,但其中来自客户端的值都应视作不可信输入。

  • $_GET:读取 URL 查询参数,例如 ?page=2。适合筛选、搜索和分页等可分享的查询条件,不适合放密码、密钥等机密。
  • $_POST:读取常见表单提交的数据。POST 不等于加密;没有 HTTPS 时,传输内容仍可能被窃听。
  • $_SERVER:包含请求方法、主机等服务器与请求信息。部分字段可能受客户端或代理影响,不能一概视为可信身份凭据。

读取一个表单字段,可以先提供默认值,再验证类型和内容:

<?php
$title = $_POST["title"] ?? "";

if (!is_string($title)) {
    http_response_code(400);
    exit("请求格式错误");
}

$title = trim($title);

if ($title === "") {
    http_response_code(400);
    exit("标题不能为空");
}

验证与转义是两件不同的事:验证判断数据是否符合业务规则;转义则根据最终输出的上下文,降低数据被解释为代码的风险。对 HTML 文本节点,常用 htmlspecialchars()。它不能替代 SQL 预处理语句,也不能作为所有输出场景通用的安全处理。

四、面向对象与异常处理

PHP 支持面向对象编程。类描述对象的属性和行为,适合表达具有状态或明确职责的组件。小脚本不一定需要层层封装;随着业务复杂度增长,再把数据库访问、业务规则和页面展示分开,通常更易于维护。

<?php
class Counter
{
    private int $value = 0;

    public function increment(): void
    {
        $this->value++;
    }

    public function getValue(): int
    {
        return $this->value;
    }
}

$counter = new Counter();
$counter->increment();
echo $counter->getValue();

运行过程中可能出现异常。捕获异常时应处理可恢复的情况,记录必要信息,并避免把数据库连接字符串、文件路径或堆栈细节原样展示给普通用户。

<?php
try {
    // 执行可能失败的操作
    echo "操作完成";
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
    echo "服务器暂时无法处理请求";
}

五、项目实战:用 PHP 和 SQLite 制作待办清单

这个项目提供一个最小但完整的网页:显示待办事项、添加事项、删除事项。它适合用来学习表单处理、数据库和页面输出之间的关系。项目不包含用户登录、权限管理和多用户隔离,因此不应直接当成多人使用的生产应用。

1. 创建项目目录

新建一个目录,例如 php-todo,在里面创建 index.php。数据库文件会在首次运行时自动创建,并保存在同一目录。运行 PHP 的进程需要对该目录有写权限。

以下代码要求 PHP 启用 PDO SQLite 扩展。代码将数据库操作使用预处理语句,并为表单加入会话令牌,以降低跨站请求伪造(CSRF)风险。令牌校验不能替代用户身份验证和权限控制。

<?php
declare(strict_types=1);

session_start();

$dbFile = __DIR__ . "/todo.sqlite";

try {
    $pdo = new PDO("sqlite:" . $dbFile);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $pdo->exec(
        "CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        )"
    );
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
    exit("数据库初始化失败,请检查 PHP 扩展和目录权限。");
}

if (!isset($_SESSION["csrf_token"])) {
    $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}

function escapeHtml(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}

function requireValidCsrfToken(): void
{
    $token = $_POST["csrf_token"] ?? null;
    $sessionToken = $_SESSION["csrf_token"] ?? null;

    if (
        !is_string($token)
        || !is_string($sessionToken)
        || !hash_equals($sessionToken, $token)
    ) {
        http_response_code(403);
        exit("请求校验失败,请刷新页面后重试。");
    }
}

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    requireValidCsrfToken();

    $action = $_POST["action"] ?? "";

    if (!is_string($action)) {
        http_response_code(400);
        exit("无效请求。");
    }

    if ($action === "add") {
        $title = $_POST["title"] ?? "";

        if (!is_string($title)) {
            http_response_code(400);
            exit("标题格式无效。");
        }

        $title = trim($title);
        $length = mb_strlen($title, "UTF-8");

        if ($length < 1 || $length > 100) {
            http_response_code(400);
            exit("标题长度必须为 1 到 100 个字符。");
        }

        $statement = $pdo->prepare(
            "INSERT INTO todos (title) VALUES (:title)"
        );
        $statement->execute(["title" => $title]);
    } elseif ($action === "delete") {
        $id = $_POST["id"] ?? "";

        if (!is_string($id) || !ctype_digit($id)) {
            http_response_code(400);
            exit("无效的事项编号。");
        }

        $statement = $pdo->prepare(
            "DELETE FROM todos WHERE id = :id"
        );
        $statement->execute(["id" => $id]);
    } else {
        http_response_code(400);
        exit("未知操作。");
    }

    header("Location: /");
    exit;
}

$statement = $pdo->query(
    "SELECT id, title, created_at FROM todos ORDER BY id DESC"
);
$todos = $statement->fetchAll(PDO::FETCH_ASSOC);
$csrfToken = $_SESSION["csrf_token"];
?>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>待办清单</title>
</head>
<body>
    <h1>待办清单</h1>

    <form method="post" action="/">
        <input type="hidden" name="csrf_token"
               value="<?= escapeHtml($csrfToken) ?>">
        <input type="hidden" name="action" value="add">
        <label for="title">新事项</label>
        <input id="title" name="title" maxlength="100" required>
        <button type="submit">添加</button>
    </form>

    <?php if ($todos === []): ?>
        <p>目前没有待办事项。</p>
    <?php else: ?>
        <ul>
            <?php foreach ($todos as $todo): ?>
                <li>
                    <span><?= escapeHtml($todo["title"]) ?></span>
                    <form method="post" action="/">
                        <input type="hidden" name="csrf_token"
                               value="<?= escapeHtml($csrfToken) ?>">
                        <input type="hidden" name="action" value="delete">
                        <input type="hidden" name="id"
                               value="<?= escapeHtml((string) $todo["id"]) ?>">
                        <button type="submit">删除</button>
                    </form>
                </li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</body>
</html>

若没有安装 mbstring 扩展,示例中的 mb_strlen() 会不可用。应安装并启用该扩展后再运行,或按项目需求选用其他经过确认的 Unicode 字符长度方案;不要简单用字节长度函数替代字符长度检查。HTML 输入框的 maxlength 只是客户端约束,服务器端仍需独立校验。

2. 启动与测试

在 php-todo 目录中运行:

php -S 127.0.0.1:8000

访问 http://127.0.0.1:8000,添加一条事项,确认页面显示后再点击删除。关闭服务器并重新启动,数据仍会保留,因为它写入了本地 SQLite 文件 todo.sqlite。

3. 代码中的安全做法与适用边界

  • 数据库查询使用预处理语句。用户输入通过参数绑定传给 SQL,避免直接把输入拼接进查询语句。预处理语句保护的是数据值;表名、列名等 SQL 结构不能简单作为参数绑定,动态结构应使用固定白名单。
  • 输出 HTML 时进行转义。escapeHtml() 将文本按 HTML 上下文处理,适用于页面文本和带引号的 HTML 属性。若数据放在 JavaScript、CSS 或 URL 等其他上下文,不能想当然地沿用同一种转义方式。
  • 使用 CSRF 令牌。浏览器会自动携带部分会话信息,攻击者可能诱导已登录用户发送非预期请求。令牌校验可以降低这类风险,但不会验证用户是否有权操作某条数据;真实应用还必须检查登录身份和资源权限。
  • POST 后重定向。操作成功后返回重定向,减少浏览器刷新页面时重复提交表单的情况。这是常见的页面交互模式,但不保证网络重试或其他重复请求一定不会造成重复写入。
  • 错误细节写日志,不直接展示。页面展示通用错误信息,详细异常写入服务器日志。生产环境还应配置日志轮转、访问权限和错误展示策略。

项目使用 SQLite 是为了减少初学者的环境配置步骤。SQLite 适合本地应用、原型和负载适中的场景,但并非所有并发、备份和运维需求都适合用它解决。需要多台应用服务器共享数据、复杂并发控制或集中运维时,通常要评估 MySQL、PostgreSQL 等数据库。换数据库时,SQL 语法、驱动、事务与部署方式也要一并检查。

六、常见错误与排查方法

1. 页面显示空白

可能是 PHP 语法错误、扩展缺失或异常被隐藏。先在开发环境查看 PHP 和 Web 服务器日志,并用命令行检查语法:

php -l index.php

不要在生产页面公开显示完整错误堆栈。调试完成后,也不要忘记关闭面向访客的错误输出。

2. 修改数据库没有效果

确认 PHP 进程实际使用的数据库文件路径和目录写权限。相对路径可能随启动目录变化,因此示例使用 __DIR__ 构造数据库文件路径。还要检查表名、字段名和 SQL 错误;PDO 异常模式有助于尽早发现问题。

3. 表单数据类型不符合预期

表单值通常以字符串形式到达服务器。即使界面使用数字输入框,也不能假定客户端一定提交数字。应在服务器端检查类型、格式、范围和业务规则;对于整数编号,既要拒绝非法字符串,也要在业务层确认记录确实存在并且当前用户有权限操作。

4. 出现乱码或字符长度不正确

页面应使用 UTF-8,数据库连接和数据源也应保持一致。处理中文长度时,PHP 的 strlen() 返回的是字节数,不是用户通常理解的字符数;示例使用 mb_strlen(),因此依赖 mbstring 扩展。

5. HTML 转义与 SQL 防护混为一谈

把输入先做 HTML 转义再存进数据库,不能防止 SQL 注入,还可能导致展示数据出现多余实体字符。正确做法是:验证业务数据,使用 PDO 预处理语句访问数据库,在输出到 HTML 时按上下文进行转义。不要把某一种“清洗函数”当作对所有风险都有效的通用方案。

七、从示例走向真实项目

待办清单的核心流程可以概括为:接收请求、验证输入、执行数据库操作、读取数据、转义输出。项目变大后,可进一步将代码拆分为路由、控制器、服务、数据访问层和模板,但拆分的目的应是控制复杂度,而不是为了使用设计模式而增加不必要的文件。

上线前还应补充:身份认证和逐条资源授权、HTTPS、会话 Cookie 安全属性、错误和审计日志、数据库备份、输入长度限制、依赖更新以及自动化测试。密码应使用 PHP 提供的密码哈希接口保存和验证,不能明文保存,也不应自行设计加密算法。上传文件、执行外部命令和处理 HTML 富文本等功能有各自的安全边界,不能直接套用本文的表单示例。

掌握 PHP 不只是记住语法,更重要的是理解数据从哪里来、将被解释成什么,以及失败时如何处理。先用小项目练习请求、数据库和输出的完整链路,再逐步补充权限、测试和部署知识,能够更稳妥地从基础语法走向实际开发。

© 版权声明

相关文章