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









