PHP 基本语法 05|流程控制:if、switch、match 与循环
if / elseif / else
<?php
$score = 85;
if ($score >= 90) {
echo "优秀";
} elseif ($score >= 80) {
echo "良好";
} elseif ($score >= 60) {
echo "及格";
} else {
echo "不及格";
}
注意 elseif 是一个词,else if 是两个词,两者在普通语法里等价,但在替代语法(if: ... endif)里只能用 elseif。统一写成一个词可以少踩坑。
替代语法:给模板用的
<div>
<?php if ($user): ?>
<p>欢迎,<?= htmlspecialchars($user['name']) ?></p>
<?php if ($user['is_vip']): ?>
<span>VIP</span>
<?php endif; ?>
<?php else: ?>
<p><a href="/login">请登录</a></p>
<?php endif; ?>
</div>
: 替代 {,endif / endforeach / endfor / endwhile / endswitch 收尾。这种写法在纯 PHP 模板(不走 Blade/Twig)时是标准做法,因为它让 HTML 缩进不被花括号打断。
把"条件不满足就退出"写成卫语句
<?php
function process(array $order): void
{
if (!$order) {
return;
}
if ($order['status'] !== 'paid') {
throw new RuntimeException('订单未支付');
}
// 主逻辑,保持低缩进
}
嵌套 if 三层以上的代码基本都能用卫语句拍平。这比业务逻辑本身更影响可读性。
何时用 switch
<?php
$action = 'edit';
switch ($action) {
case 'create':
echo "新建";
break;
case 'edit':
case 'update': // 多个 case 共享同一段逻辑
echo "修改";
break;
case 'delete':
echo "删除";
break;
default:
echo "未知操作";
}
switch 的三个技术事实:
一、它用的是松散比较 ==。
<?php
switch ("1") {
case 1:
echo "匹配到了 int 1"; // 会进来,因为 "1" == 1
break;
}
这在从 HTTP 参数取值(永远是字符串)时是方便,但在做严格判断时是隐患。
二、没有 break 就会穿透(fall through)。 这有时是刻意设计(上面多个 case 共享逻辑的写法),但多数时候是 bug。所有检查工具都会报这条。
三、switch 里的 continue 等于 break。 这是 PHP 的历史遗留:switch 在语言层面被认为是一种循环结构,continue 在它内部的行为和 break 一样。PHP 7.3 起会给出警告:"continue" targeting switch is equivalent to "break"。在 switch 里想跳出外层循环,必须写 break 2,不要写 continue 2 之类容易误解的东西。
match:PHP 8.0 之后更适合做分支映射
<?php
$status = 2;
$label = match ($status) {
1 => '待支付',
2, 3 => '已支付', // 多个值共享一个结果
4, 5 => '已发货',
default => '未知',
};
和 switch 的差别,逐条对齐:
switch |
match |
|
|---|---|---|
| 是表达式吗 | 否,语句 | 是,可直接赋值/返回 |
| 比较方式 | == 松散 |
=== 严格 |
需要break |
需要 | 不需要,自动结束 |
| 未匹配且无 default | 静默跳过 | 抛UnhandledMatchError |
| 能匹配表达式 | case 只能是常量表达式 |
case 可以是任意表达式 |
这里有一个实战价值很高的用法——match (true) 做区间判断:
<?php
function grade(int $score): string
{
return match (true) {
$score >= 90 => '优秀',
$score >= 80 => '良好',
$score >= 60 => '及格',
default => '不及格',
};
}
比 if/elseif 链短得多,且返回值直接可用。
match 没有 default 且没匹配上时会抛异常,这是它的优势:漏掉分支立刻炸,不会静默返回 null 让 bug 跑到下游。
while 与 do-while
<?php
$i = 0;
while ($i < 5) {
echo $i++;
}
// do-while 至少执行一次
$i = 10;
do {
echo $i--;
} while ($i > 0);
一个常见的实用写法是"读文件/读结果集直到空":
<?php
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo $row['name'], PHP_EOL;
}
这里 while 的条件是赋值表达式的值,读到末尾返回 false 就退出。注意要用 !== false 而不是 !== null,PDO 的 fetch 在无数据时返回 false。
for 循环
<?php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
三个表达式都可以省略,写成死循环:
<?php
for (;;) {
// 靠内部 break 退出
}
性能上的一个提醒:不要在循环条件里反复调用函数。
<?php
// 慢:每次迭代都调一次 count()
for ($i = 0; $i < count($arr); $i++) { }
// 快:只算一次
for ($i = 0, $n = count($arr); $i < $n; $i++) { }
不过对于"遍历整个数组"这种需求,foreach 永远比 for 快,因为 foreach 走的是内部指针,不重复做哈希查找。
<?php
foreach ($arr as $v) { } // 推荐
foreach ($arr as $k => $v) { }
foreach ($arr as &$v) { } // 需要修改原数组时
break 与 continue 的层级
<?php
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($j === 2) {
break 2; // 跳出两层循环
}
if ($i === 1) {
continue 2; // 跳过两层,直接进下一次外层迭代
}
}
}
break N / continue N 能跳出多层,但超过 2 层就说明结构该重构了。多层跳出往往是"需要提前返回"的信号,包个函数然后 return 更清楚。
循环里做数据库操作是致命写法
<?php
// ✗ 典型的 N+1 查询
foreach ($ids as $id) {
$row = $pdo->query("SELECT * FROM users WHERE id = $id")->fetch();
}
// ✓ 一次性取回
$in = implode(',', array_map('intval', $ids));
$rows = $pdo->query("SELECT * FROM users WHERE id IN ($in)")->fetchAll();
一个循环一千次查询,每次 2ms,加起来就是 2 秒。这是线上慢接口最常见的原因,比任何语法细节都重要。
goto:知道它存在就行
<?php
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
if ($i * $j > 20) {
goto done;
}
}
}
done:
echo "结束";
goto 只能跳到同一个文件、同一个函数内的标签,不能跳进循环或跳出函数。它在 PHP 里极少使用,用在"多层循环里跳出的替代方案"上勉强说得过去,但更好的做法是抽函数 + return。
declare(strict_types=1) 与流程控制无关,但必须放在文件第一行
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
add("1", 2); // ✗ TypeError —— 严格模式下不做隐式转换
这条指令必须写在文件的最顶端(<?php 之后、命名空间之前),只对当前文件内的函数调用生效(对被调用方定义在别的文件里的函数,看调用方文件是否开启。准确地说:严格模式按"调用发生的文件"决定)。这件事放到函数篇细说,这里只需要知道它不是全局开关。
条件写法的几个实用技巧
用 ?? 和 ?: 简化取值
<?php
$page = (int)($_GET['page'] ?? 1); // 不存在就给默认
$name = $input ?: '匿名'; // 空字符串/0 也给默认
$flag = isset($x) && $x > 0; // 组合条件
注意 ?: 和 ?? 的区别:?: 判断的是"假值"(0、""、"0" 也会走默认),?? 只判断 null。
用 in_array 代替一长串 ||
<?php
// ✗
if ($status === 'paid' || $status === 'shipped' || $status === 'done') { }
// ✓
if (in_array($status, ['paid', 'shipped', 'done'], true)) { }
提前返回代替嵌套
<?php
// ✗
function check($req) {
if (isset($req['id'])) {
if ($req['id'] > 0) {
if ($req['type'] === 'a') {
return true;
}
}
}
return false;
}
// ✓
function check2($req): bool {
if (!isset($req['id']) || $req['id'] <= 0) return false;
return $req['type'] === 'a';
}
坑清单
switch用的是==,字符串"1"会匹配到case 1。switch里continue等于break,要跳循环得用break 2。switch忘写break导致穿透。match漏了分支且没default,运行到就抛UnhandledMatchError。- 循环条件里
count()每次重算。 for循环里用array_shift,O(n²)。- 循环里查数据库,N+1。
?:把0和"0"也当空处理,金额、数量场景要小心。