Skip to content

assert 断言

概述

assert() 是 PHP 的断言函数,用于在开发阶段验证代码假设。如果断言表达式为 false,PHP 会抛出异常或发出警告。在 PHP 8.0 中,assert 的行为发生了重大变更——不再将字符串参数作为 PHP 代码执行,使其更加安全。

PHP 版本说明

  • assert() 自 PHP 4 起可用
  • PHP 7.0:断言失败默认抛出 AssertionError(而非 Warning)
  • PHP 8.0:不再执行字符串参数(安全改进),字符串参数等于 false
  • assert.expect_exception:PHP 7.0 废弃,PHP 8.0 移除
  • assert.exception:默认 1(抛出异常),设为 0 则发出警告

基础概念

断言的作用

断言用于验证开发者的假设,而非验证用户输入。断言失败的预期行为是"代码有 Bug",而不是"用户输入有误"。

场景使用断言使用条件验证
验证函数参数范围不适合适合(抛出 ValueError
验证内部不变量适合不适合
验证开发者假设适合不适合
验证返回值约定适合不适合

assert 与 assert_options 配置

ini
; php.ini 配置
assert.active = 1           ; 启用断言(默认 1)
assert.exception = 1        ; 断言失败抛出异常(默认 1)
assert.bail = 0             ; 断言失败时终止脚本(默认 0)
assert.warning = 1          ; 断言失败发出 Warning(PHP 7.0 之前)
assert.callback = ""        ; 断言失败回调函数

语法与代码

基本断言用法

php
<?php

declare(strict_types=1);

// 基本语法:assert(expression, description)
$age = 25;

// 简单断言
assert($age > 0, '年龄必须大于 0');

// 复杂表达式
assert(
    is_int($age) && $age >= 0 && $age <= 150,
    '年龄必须是 0~150 之间的整数'
);

// 断言不会在生产环境中执行(assert.active=0 时)
// 但表达式中的副作用仍然存在,因此不应在断言中使用有副作用的表达式

PHP 8.0+ 断言行为

php
<?php

declare(strict_types=1);

// PHP 8.0+ 断言失败抛出 AssertionError(默认行为)
try {
    $value = -1;
    assert($value >= 0, '值必须非负');
} catch (\AssertionError $e) {
    echo $e->getMessage(); // "值必须非负"
}

// PHP 8.0+ 中,字符串参数视为断言失败
assert('1 === 1'); // PHP 8.0+ → 失败(字符串不是 bool)
assert(true);      // 成功

// PHP 8.0+ 推荐使用表达式
assert(1 === 1, '数学正确'); // 成功

PHP 8.0 重大变更

在 PHP 7.x 中,assert('1 === 1') 会将字符串作为 PHP 代码执行。PHP 8.0 起这种行为被移除,字符串参数直接被视为断言失败。这是一个安全性改进,防止通过字符串断言执行任意代码。

断言回调函数

php
<?php

declare(strict_types=1);

// 自定义断言回调
assert_options(ASSERT_CALLBACK, function (string $file, int $line, ?string $assertion): void {
    $msg = "断言失败 in {$file}:{$line}";
    if ($assertion !== null) {
        $msg .= " — {$assertion}";
    }
    error_log($msg);

    if (php_sapi_name() !== 'cli') {
        http_response_code(500);
        echo json_encode(['error' => 'Internal assertion failed']);
    }
});

// 触发回调
assert(false, '数据库连接不应为 null');

在类方法中使用断言

php
<?php

declare(strict_types=1);

class UserRepository
{
    private array $users = [];

    public function add(User $user): void
    {
        // 断言:用户 ID 必须唯一(开发期验证)
        $id = $user->id;
        assert(
            !isset($this->users[$id]),
            "用户 ID {$id} 已存在,违反唯一性约束"
        );

        $this->users[$id] = $user;
    }

    public function find(int $id): ?User
    {
        // 断言:ID 必须为正数
        assert($id > 0, "用户 ID 必须为正数,收到: {$id}");

        return $this->users[$id] ?? null;
    }

    public function count(): int
    {
        // 断言:内部状态一致性验证
        $actualCount = count($this->users);
        assert(
            $actualCount >= 0,
            "用户数量不能为负数: {$actualCount}"
        );
        return $actualCount;
    }
}

// 使用
$user = new User(id: 1, name: 'Alice');
$repo = new UserRepository();
$repo->add($user);
$repo->add($user); // AssertionError: 用户 ID 1 已存在

assert.exception 配置

php
<?php

declare(strict_types=1);

// 方式 1:通过 assert_options 设置
assert_options(ASSERT_EXCEPTION, 0); // 断言失败发出 Warning,不抛异常

// 方式 2:通过 ini_set
ini_set('assert.exception', '0');

// 配置为 0 时,断言失败产生 Warning
assert(false, '这会产生 Warning');

// 恢复默认(抛出异常)
assert_options(ASSERT_EXCEPTION, 1);

详细说明

assert() 函数签名

php
assert(
    mixed $assertion,
    string|Throwable $description = ""
): bool
参数类型说明
$assertionmixed断言表达式(PHP 8.0+,字符串视为 false
$descriptionstring|Throwable失败描述或自定义异常

PHP 5.x → 7.x → 8.0 演进

特性PHP 5.xPHP 7.0~7.4PHP 8.0+
默认行为WarningAssertionErrorAssertionError
字符串参数作为 PHP 代码执行作为 PHP 代码执行视为 false
assert.exception不存在默认 1(抛异常)默认 1
安全性低(代码注入风险)

断言 vs 异常 vs 条件验证

php
<?php

declare(strict_types=1);

function calculateDiscount(int $price, int $percent): int
{
    // 条件验证:对用户输入,使用 if + 抛出异常
    if ($price < 0) {
        throw new \ValueError("价格不能为负数: {$price}");
    }
    if ($percent < 0 || $percent > 100) {
        throw new \ValueError("折扣必须在 0~100 之间: {$percent}");
    }

    $discounted = (int) ($price * (100 - $percent) / 100);

    // 断言:验证内部计算结果(开发者假设)
    assert(
        $discounted >= 0 && $discounted <= $price,
        '折扣计算结果必须在合理范围内'
    );

    return $discounted;
}

description 参数传入 Throwable

php
<?php

declare(strict_types=1);

class InvalidStateException extends \RuntimeException {}

// 传入 Throwable 作为 description
assert(
    $state === 'active',
    new InvalidStateException('用户状态必须为 active')
);

// 等价于
// if ($state !== 'active') {
//     throw new InvalidStateException('用户状态必须为 active');
// }

实战示例

开发期的契约式设计

php
<?php

declare(strict_types=1);

final class Money
{
    private readonly int $amount;

    public function __construct(int $amount)
    {
        // 前置条件:使用条件验证(面向用户)
        if ($amount < 0) {
            throw new \ValueError("金额不能为负数: {$amount}");
        }

        $this->amount = $amount;
    }

    public function add(Money $other): Money
    {
        $sum = $this->amount + $other->amount;

        // 后置条件:使用断言(面向开发者)
        assert(
            $sum >= $this->amount && $sum >= $other->amount,
            '加法结果不应小于任一操作数'
        );

        return new Money($sum);
    }

    public function multiply(int $factor): Money
    {
        assert($factor >= 0, '乘数不能为负');

        $product = $this->amount * $factor;

        // 不变量验证
        assert(
            $product >= 0,
            '金额乘法结果不能为负'
        );

        return new Money($product);
    }

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

// 开发期使用断言验证假设
$price = new Money(100);
$discounted = $price->multiply(-1); // AssertionError: 乘数不能为负

注意事项

  1. 断言不是输入验证:永远不要用 assert 验证用户输入。断言可能在生产环境中被禁用,导致安全检查被跳过。

  2. PHP 8.0 不再执行字符串断言:如果从 PHP 7.x 升级到 8.0,所有 assert('expression') 形式的断言都需要改为表达式形式。

  3. 断言中的副作用:不要在断言表达式中编写有副作用的代码(如 $x = assert($y = compute())),因为生产环境断言可能被禁用。

  4. assert.bail = 1 很危险:此选项会在断言失败时立即终止脚本,即使有自定义异常处理器也不会执行。

  5. PHPUnit 中的 assert 是不同的:PHPUnit 的 assertTrue() 等断言方法是测试框架的一部分,与 PHP 内置的 assert() 无关。

最佳实践

推荐做法

  1. 断言仅用于开发期验证:验证开发者假设、不变量、前后条件,而非用户输入
  2. 生产环境禁用断言执行:设置 assert.active = 0,但保留断言语法(避免表达式副作用)
  3. 使用描述性消息:断言失败时提供清晰的上下文信息,帮助快速定位问题
  4. 使用 Throwable 作为 description:可以抛出自定义异常类型,获得更好的异常分类
  5. 配合静态分析工具:PhpStan、Psalm 等工具可以在编译期发现断言能覆盖的问题
php
<?php

declare(strict_types=1);

// 生产环境配置建议
// php.ini
// assert.active = 1
// assert.exception = 1
// zend.assertions = -1  ; 生产环境使用 -1(编译时移除断言代码)

// 开发环境
// zend.assertions = 1     ; 启用断言

// 测试环境
// zend.assertions = 1     ; 启用断言

进阶用法

调试与测试技巧

php
<?php
declare(strict_types=1);

// 单元测试辅助函数
function createTestResource(): mixed
{
    return match (true) {
        default => new stdClass(),
    };
}

// 调试输出函数
function debugOutput(mixed , string  = ''): void
{
     =  ? ": " : '';
     .= print_r(, true);
    fwrite(STDERR,  . "\n");
}

// 性能基准测试
function benchmark(callable , int  = 1000): float
{
     = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e9;
}

日志记录实践

php
<?php
declare(strict_types=1);

/**
 * 简易日志记录器
 */
class SimpleLogger
{
    private string $logFile;
    private string $level = 'INFO';

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('WARNING', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    private function log(string $level, string $message, array $context): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

配置与环境检测

php
<?php
declare(strict_types=1);

// 环境检测工具
class EnvironmentChecker
{
    public static function checkRequirements(array $requirements): array
    {
        $results = [];
        foreach ($requirements as $name => $check) {
            $results[$name] = is_callable($check) ? $check() : false;
        }
        return $results;
    }

    public static function getSystemInfo(): array
    {
        return [
            'php_version' => PHP_VERSION,
            'os' => PHP_OS,
            'sapi' => PHP_SAPI,
            'memory_limit' => ini_get('memory_limit'),
            'max_execution_time' => ini_get('max_execution_time'),
            'loaded_extensions' => get_loaded_extensions(),
        ];
    }
}

常见问题排查

问题可能原因解决方案
连接超时网络问题/配置错误检查配置,增加超时时间
权限不足文件/目录权限使用 chmod/chown 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 8.0__construct(public $x)
php
<?php
declare(strict_types=1);

// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
    if (version_compare(PHP_VERSION, $minVersion, '<')) {
        throw new RuntimeException(
            sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
        );
    }
}

ensureVersion('8.1.0');

参考链接