PHP 零基础教程

PHP 异常处理 (try...catch)

PHP 中的异常处理(Exception handling)提供了一种结构化且面向对象的方式,用于管理脚本执行期间出现的意外错误或异常情况。与传统的错误报告(可能会直接中断脚本执行或仅仅记录一个警告)不同,异常处理允许开发者“捕获(catch)”这些事件并执行特定的恢复逻辑,从而确保应用程序更加健壮且用户体验更好。try...catch 代码块是 PHP 中实现异常处理的核心基础结构。

1. 理解 try...catch 代码块

try...catch 代码块的工作原理是划定一段可能会发生异常的代码区域。如果在 try 块内部“抛出(thrown)”了一个异常,程序的执行流会立即跳转到对应的 catch 块。catch 块会指定它能够处理的异常类型,并提供捕获该异常后需要执行的代码。

一个 try 块之后必须紧跟至少一个 catch 块或 finally 块(finally 块将在后续章节中详细介绍)。

1.1 基础语法

基础语法包含一个 try 关键字,后跟一对包含受监控代码的大括号 {}。在 try 块之后紧接着定义一个或多个 catch 块。每个 catch 块都需要声明它能处理的异常类型的类型提示(例如 ExceptionRuntimeException 或自定义的异常类),以及一个用于接收异常对象的变量。

<?php
try {
    // 可能抛出异常的代码
    // 例如,尝试除以零
    $numerator = 10;
    $denominator = 0;
    
    if ($denominator === 0) {
        throw new Exception("不允许除以零。");
    }
    
    $result = $numerator / $denominator;
    echo "结果: " . $result;
    
} catch (Exception $e) {
    // 处理异常的代码
    // $e 变量包含了异常对象
    echo "发生了一个错误: " . $e->getMessage() . "\n";
    echo "错误所在行: " . $e->getLine() . "\n";
    echo "错误所在文件: " . $e->getFile() . "\n";
    // 在调试时,你可能还需要记录堆栈追踪:$e->getTraceAsString()
}

echo "异常处理结束后,脚本继续执行。\n";
?>

在这个例子中,try 块尝试执行一次除法运算。在进行除法之前,它会检查分母是否为零。如果是零,则创建一个带有描述性消息的新 Exception 对象,并使用 throw 关键字将其抛出。这会立即将控制权转移给 catch (Exception $e) 块。在 catch 块内部,我们从 $e 对象中提取异常消息、行号和文件名并显示出来。在 try...catch 结构执行完毕后,脚本会继续往下执行。

2. throw 关键字

throw 关键字用于显式地引发(抛出)一个异常。当异常被抛出时,代码的正常执行流程会被中断。然后,PHP 会开始寻找能够处理该类型异常的、距离最近的闭合 catch 块。如果找不到匹配的 catch 块,PHP 脚本将终止运行,并显示一个“未捕获的异常 (uncaught exception)”错误。

你可以抛出任何实现了 PHP Throwable 接口的对象。最常用的异常基类是 Exception

<?php
function processUserData($data) {
    if (!is_array($data)) {
        throw new InvalidArgumentException("用户数据必须是一个数组。");
    }
    
    if (empty($data['username'])) {
        throw new Exception("用户名不能为空。"); // 为了演示,这里使用通用的 Exception
    }
    
    if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        throw new Exception("无效的邮箱格式。");
    }
    
    // 模拟数据处理
    echo "用户 " . $data['username'] . " 的数据已成功处理。\n";
}

try {
    // 示例 1:有效数据
    processUserData(['username' => 'john.doe', 'email' => 'john@example.com']);
    
    // 示例 2:无效的邮箱格式
    processUserData(['username' => 'jane.doe', 'email' => 'invalid-email']);
    
} catch (InvalidArgumentException $e) {
    echo "捕获到 InvalidArgumentException: " . $e->getMessage() . "\n";
} catch (Exception $e) { // 捕获任何其他类型的 Exception
    echo "捕获到通用的 Exception: " . $e->getMessage() . "\n";
}

echo "尝试处理用户数据后,脚本继续执行。\n";
?>

在这个演示中,processUserData 函数会根据输入验证的结果抛出特定的异常。try...catch 块尝试使用不同的输入来执行这个函数。请注意 catch 块的顺序:具体的异常(如 InvalidArgumentException)必须在更通用的异常(如 Exception)之前被捕获。这一点至关重要,因为 Exception 是许多其他异常的基类,如果把它放在前面,它就会拦截所有的异常,导致后面更具体的错误处理代码永远无法执行。

3. 捕获多种异常类型

一个 try 块后面可以跟多个 catch 块,每个都旨在处理不同类型的异常。PHP 会执行第一个其声明的异常类型与抛出异常类型相匹配(或是其父类)的 catch 块。

<?php
// 自定义异常类
class DatabaseConnectionException extends Exception {}
class QueryExecutionException extends Exception {}

function connectToDatabase($server) {
    if ($server !== "localhost") {
        throw new DatabaseConnectionException("无法连接到位于 " . $server . " 的数据库");
    }
    echo "成功连接到数据库。\n";
}

function executeQuery($query) {
    if (strpos($query, "DROP TABLE") !== false) {
        throw new QueryExecutionException("被禁止的查询: " . $query);
    }
    echo "查询已执行: " . $query . "\n";
}

try {
    // 尝试 1:成功的操作
    connectToDatabase("localhost");
    executeQuery("SELECT * FROM users");
    
    // 尝试 2:数据库连接错误
    // connectToDatabase("remotehost");
    
    // 尝试 3:查询执行错误
    // executeQuery("DROP TABLE users");
    
} catch (DatabaseConnectionException $e) {
    echo "数据库错误: " . $e->getMessage() . "\n";
    // 记录错误,通知管理员等
} catch (QueryExecutionException $e) {
    echo "查询错误: " . $e->getMessage() . "\n";
    // 防止敏感数据泄露,记录危险查询尝试
} catch (Exception $e) {
    echo "发生了一个意外错误: " . $e->getMessage() . "\n";
    // 针对任何其他异常的通用后备方案
}

echo "数据库操作完成后,应用程序继续执行。\n";
?>

在这个详尽的例子中,我们定义了两个自定义异常类:DatabaseConnectionExceptionQueryExecutionException,它们都继承自 Exception。这使得我们能够进行更精细的错误处理。try 块尝试执行数据库操作,而不同的 catch 块则准备好应对特定的问题。这展示了如何以受控的方式对不同类型的问题进行分类和响应。

4. 实战演练

实现 try...catch 块对于构建能够优雅应对意外情况的健壮应用程序至关重要。

4.1 文件操作与异常处理

文件系统操作很容易出现各种错误,例如文件不存在、权限不足或磁盘已满。try...catch 块提供了一种清晰的方式来管理这些情况。

<?php
function readFileContents($filePath) {
    if (!file_exists($filePath)) {
        throw new Exception("文件未找到: " . $filePath);
    }
    if (!is_readable($filePath)) {
        throw new Exception("没有权限读取文件: " . $filePath);
    }
    
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new Exception("打开文件失败: " . $filePath);
    }
    
    $contents = fread($handle, filesize($filePath));
    if ($contents === false) {
        throw new Exception("读取文件内容失败: " . $filePath);
    }
    
    fclose($handle);
    return $contents;
}

// 创建一个测试用的虚拟文件
file_put_contents('test_file.txt', '这是一些测试内容。');
chmod('unreadable_file.txt', 0000); // 创建一个不可读的文件

try {
    echo "--- 读取 'test_file.txt' ---\n";
    $content = readFileContents('test_file.txt');
    echo "文件内容: " . $content . "\n\n";
    
    echo "--- 读取 'non_existent_file.txt' ---\n";
    $content = readFileContents('non_existent_file.txt'); // 这将抛出 '文件未找到'
    echo "文件内容: " . $content . "\n\n"; // 这行代码将不会被执行
    
} catch (Exception $e) {
    echo "捕获到文件错误: " . $e->getMessage() . "\n\n";
}

try {
    echo "--- 读取 'unreadable_file.txt' ---\n";
    $content = readFileContents('unreadable_file.txt'); // 这将抛出 '没有权限' (如果文件已创建)
    echo "文件内容: " . $content . "\n\n"; // 这行代码将不会被执行
    
} catch (Exception $e) {
    echo "捕获到文件错误: " . $e->getMessage() . "\n\n";
} finally {
    // 无论是否发生异常,都清理测试文件
    if (file_exists('test_file.txt')) unlink('test_file.txt');
    if (file_exists('unreadable_file.txt')) unlink('unreadable_file.txt');
}

echo "文件操作尝试完成。\n";
?>

这个例子将文件操作封装在一个函数中,并针对不同的失败点使用了 throw new Exception。随后,try...catch 块处理这些特定情况,提供有意义的反馈,而不是抛出原始的 PHP 警告。finally 块(此处仅作简要介绍,后续会详细讲解)确保无论是否发生异常,清理工作都会被执行。

4.2 使用自定义异常验证输入

创建自定义异常类可以让错误处理在语义上更清晰,使你的代码更易于阅读和维护。

<?php
class InvalidInputException extends Exception {}
class MissingFieldException extends InvalidInputException {}
class InvalidFormatException extends InvalidInputException {}

function registerUser($userData) {
    if (!is_array($userData)) {
        throw new InvalidInputException("注册数据必须是一个数组。");
    }
    if (empty($userData['username'])) {
        throw new MissingFieldException("用户名是必填项。");
    }
    if (empty($userData['password'])) {
        throw new MissingFieldException("密码是必填项。");
    }
    if (!preg_match('/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/', $userData['password'])) {
        throw new InvalidFormatException("密码长度至少为8个字符,且必须包含至少一个字母和一个数字。");
    }
    
    // 模拟用户创建过程
    echo "用户 '" . $userData['username'] . "' 注册成功。\n";
}

try {
    // 成功的注册
    registerUser(['username' => 'dev_user', 'password' => 'SecureP@ss1']);
    
    // 缺失用户名
    registerUser(['password' => 'AnotherP@ss2']);
    
} catch (MissingFieldException $e) {
    echo "注册错误 (字段缺失): " . $e->getMessage() . "\n";
} catch (InvalidFormatException $e) {
    echo "注册错误 (格式无效): " . $e->getMessage() . "\n";
} catch (InvalidInputException $e) { 
    // 捕获上面未捕获的任何一般的 InvalidInputException
    echo "注册错误 (无效输入): " . $e->getMessage() . "\n";
} catch (Exception $e) { 
    // 通用的兜底方案
    echo "发生了一个意外的注册错误: " . $e->getMessage() . "\n";
}

echo "注册尝试完成。\n";
?>

在这里,我们继承 Exception 创建了 InvalidInputException,然后进一步继承它创建了 MissingFieldExceptionInvalidFormatException。这构建了一个异常层级结构。当 registerUser 抛出 MissingFieldException 时,特定的 catch (MissingFieldException $e) 块将被执行。这使得我们能够精细化地控制不同类型输入错误的处理方式。