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';
}

坑清单

  1. switch 用的是 ==,字符串 "1" 会匹配到 case 1。
  2. switch 里 continue 等于 break,要跳循环得用 break 2。
  3. switch 忘写 break 导致穿透。
  4. match 漏了分支且没 default,运行到就抛 UnhandledMatchError。
  5. 循环条件里 count() 每次重算。
  6. for 循环里用 array_shift,O(n²)。
  7. 循环里查数据库,N+1。
  8. ?: 把 0 和 "0" 也当空处理,金额、数量场景要小心。