异常处理机制

什么是异常

异常(Exception)是PHP中用于处理错误和异常情况的一种机制。与传统的错误处理方式不同,异常提供了一种更加结构化和灵活的方式来处理程序运行时出现的问题。

异常是程序在执行过程中发生的、打断了正常指令流程的事件。当异常发生时,程序会创建一个异常对象,并寻找能够处理这个异常的代码。

异常 vs 错误

特性错误(Error)异常(Exception)
处理方式由错误处理器处理由try-catch块处理
可恢复性通常不可恢复可以恢复并继续执行
类型内置错误级别可以自定义异常类
面向对象不是面向对象的面向对象的机制
追踪信息有限的信息详细的堆栈跟踪

异常的基本语法

try-catch 块

<?php
try {
    // 可能抛出异常的代码
    $result = 10 / 0;
} catch (Exception $e) {
    // 处理异常的代码
    echo "捕获到异常: " . $e->getMessage();
}
?>

基本异常处理示例

<?php
function divide($a, $b) {
    if ($b == 0) {
        // 抛出异常
        throw new Exception("除数不能为零");
    }
    return $a / $b;
}

try {
    echo divide(10, 2) . "\n";  // 正常执行
    echo divide(10, 0) . "\n";  // 抛出异常
    echo "这行不会执行\n";      // 异常后的代码不会执行
} catch (Exception $e) {
    echo "捕获异常: " . $e->getMessage() . "\n";
    echo "文件: " . $e->getFile() . "\n";
    echo "行号: " . $e->getLine() . "\n";
}

echo "程序继续执行\n";
?>

Exception 类

PHP内置的Exception类提供了异常的基本功能:

<?php
$exception = new Exception("这是一个异常");

// Exception类的主要方法
echo $exception->getMessage();  // 获取异常消息
echo $exception->getCode();     // 获取异常代码
echo $exception->getFile();     // 获取抛出异常的文件
echo $exception->getLine();     // 获取抛出异常的行号
echo $exception->getTrace();    // 获取异常堆栈跟踪
echo $exception->getTraceAsString();  // 获取格式化的堆栈跟踪
echo $exception->__toString();  // 将异常转换为字符串
?>

自定义异常消息和代码

<?php
class DatabaseException extends Exception {
    public function __construct($message, $code = 0, Exception $previous = null) {
        parent::__construct($message, $code, $previous);
    }

    public function getDetailedMessage() {
        return "数据库错误: " . $this->getMessage() .
               " (代码: " . $this->getCode() . ")";
    }
}

class ValidationException extends Exception {
    private $errors;

    public function __construct($errors, $message = "验证失败", $code = 0) {
        $this->errors = $errors;
        parent::__construct($message, $code);
    }

    public function getErrors() {
        return $this->errors;
    }

    public function getFirstError() {
        return $this->errors[0] ?? '未知错误';
    }
}

// 使用自定义异常
try {
    $errors = ['name' => '姓名不能为空', 'email' => '邮箱格式不正确'];
    throw new ValidationException($errors);
} catch (ValidationException $e) {
    echo $e->getMessage() . "\n";
    echo "具体错误: " . implode(', ', $e->getErrors()) . "\n";
}
?>

多个 catch 块

<?php
class FileNotFoundException extends Exception {}
class FileReadException extends Exception {}

function readFileContent($filename) {
    if (!file_exists($filename)) {
        throw new FileNotFoundException("文件不存在: $filename");
    }

    $content = file_get_contents($filename);
    if ($content === false) {
        throw new FileReadException("无法读取文件: $filename");
    }

    return $content;
}

try {
    $content = readFileContent("non_existent_file.txt");
} catch (FileNotFoundException $e) {
    echo "文件未找到: " . $e->getMessage() . "\n";
} catch (FileReadException $e) {
    echo "读取失败: " . $e->getMessage() . "\n";
} catch (Exception $e) {
    // 捕获所有其他异常
    echo "未知错误: " . $e->getMessage() . "\n";
}
?>

finally 块

finally块无论是否发生异常都会执行,通常用于清理资源。

<?php
function processFile($filename) {
    $handle = null;
    try {
        echo "尝试打开文件\n";
        $handle = fopen($filename, 'r');

        if (!$handle) {
            throw new Exception("无法打开文件");
        }

        echo "文件处理中...\n";
        return "处理完成";

    } catch (Exception $e) {
        echo "捕获异常: " . $e->getMessage() . "\n";
        return "处理失败";
    } finally {
        // 无论是否发生异常都会执行
        if ($handle) {
            fclose($handle);
            echo "文件已关闭\n";
        }
        echo "清理工作完成\n";
    }
}

// 测试
$result = processFile("test.txt");
echo "结果: $result\n";

$result = processFile("non_existent.txt");
echo "结果: $result\n";
?>

抛出异常

throw 关键字

<?php
function validateAge($age) {
    if ($age < 0) {
        throw new InvalidArgumentException("年龄不能为负数");
    }
    if ($age > 150) {
        throw new InvalidArgumentException("年龄不能超过150");
    }
    return true;
}

function calculateBMI($weight, $height) {
    if ($height <= 0) {
        throw new RuntimeException("身高必须大于0");
    }
    if ($weight <= 0) {
        throw new RuntimeException("体重必须大于0");
    }

    return $weight / ($height * $height);
}

// 使用示例
try {
    validateAge(-5);  // 抛出异常
} catch (InvalidArgumentException $e) {
    echo "验证错误: " . $e->getMessage() . "\n";
}

try {
    $bmi = calculateBMI(70, 1.75);
    echo "BMI指数: " . round($bmi, 2) . "\n";
} catch (RuntimeException $e) {
    echo "计算错误: " . $e->getMessage() . "\n";
}
?>

异常链

<?php
class DatabaseConnectionException extends Exception {}
class QueryException extends Exception {}

function executeQuery($sql) {
    try {
        // 模拟数据库连接失败
        throw new DatabaseConnectionException("无法连接到数据库");
    } catch (DatabaseConnectionException $e) {
        // 抛出新异常,并保留原始异常
        throw new QueryException("查询执行失败: $sql", 0, $e);
    }
}

try {
    executeQuery("SELECT * FROM users");
} catch (QueryException $e) {
    echo "当前异常: " . $e->getMessage() . "\n";

    // 获取前一个异常
    $previous = $e->getPrevious();
    if ($previous) {
        echo "原始异常: " . $previous->getMessage() . "\n";
    }
}
?>

嵌套异常处理

<?php
function outerFunction() {
    try {
        echo "外层函数开始\n";
        innerFunction();
        echo "外层函数结束\n";
    } catch (Exception $e) {
        echo "外层捕获异常: " . $e->getMessage() . "\n";
        throw new Exception("外层函数处理失败", 0, $e);
    }
}

function middleFunction() {
    try {
        echo "中层函数开始\n";
        innerFunction();
        echo "中层函数结束\n";
    } catch (InvalidArgumentException $e) {
        echo "中层捕获参数异常: " . $e->getMessage() . "\n";
        // 重新抛出异常
        throw $e;
    }
}

function innerFunction() {
    try {
        echo "内层函数开始\n";
        throw new InvalidArgumentException("参数无效");
        echo "内层函数结束\n";
    } catch (RuntimeException $e) {
        echo "内层捕获运行时异常: " . $e->getMessage() . "\n";
    }
}

try {
    outerFunction();
} catch (Exception $e) {
    echo "最外层捕获异常: " . $e->getMessage() . "\n";

    // 显示完整的异常链
    $current = $e;
    $level = 0;
    while ($current) {
        echo "层级 $level: " . $current->getMessage() . "\n";
        $current = $current->getPrevious();
        $level++;
    }
}
?>

实际应用示例

1. 用户注册验证

<?php
class UserRegistrationException extends Exception {
    private $errors;

    public function __construct($errors, $message = "用户注册失败", $code = 0) {
        $this->errors = $errors;
        parent::__construct($message, $code);
    }

    public function getErrors() {
        return $this->errors;
    }
}

class UserService {
    public function registerUser($userData) {
        $errors = $this->validateUserData($userData);

        if (!empty($errors)) {
            throw new UserRegistrationException($errors);
        }

        // 检查用户是否已存在
        if ($this->userExists($userData['email'])) {
            throw new UserRegistrationException(
                ['email' => '该邮箱已被注册'],
                "用户已存在"
            );
        }

        // 创建用户
        $userId = $this->createUser($userData);
        return $userId;
    }

    private function validateUserData($userData) {
        $errors = [];

        if (empty($userData['name'])) {
            $errors['name'] = '姓名不能为空';
        } elseif (strlen($userData['name']) < 2) {
            $errors['name'] = '姓名至少2个字符';
        }

        if (empty($userData['email'])) {
            $errors['email'] = '邮箱不能为空';
        } elseif (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = '邮箱格式不正确';
        }

        if (empty($userData['password'])) {
            $errors['password'] = '密码不能为空';
        } elseif (strlen($userData['password']) < 6) {
            $errors['password'] = '密码至少6个字符';
        }

        return $errors;
    }

    private function userExists($email) {
        // 模拟数据库查询
        $existingUsers = ['user1@example.com', 'user2@example.com'];
        return in_array($email, $existingUsers);
    }

    private function createUser($userData) {
        // 模拟创建用户
        echo "用户创建成功: {$userData['name']} ({$userData['email']})\n";
        return uniqid('user_');
    }
}

// 使用示例
$userService = new UserService();

$testUsers = [
    // 测试1:数据不完整
    [
        'name' => '',
        'email' => 'invalid-email',
        'password' => '123'
    ],
    // 测试2:用户已存在
    [
        'name' => '张三',
        'email' => 'user1@example.com',
        'password' => '123456'
    ],
    // 测试3:正常注册
    [
        'name' => '李四',
        'email' => 'lisi@example.com',
        'password' => '123456'
    ]
];

foreach ($testUsers as $index => $userData) {
    echo "\n=== 测试 " . ($index + 1) . " ===\n";
    try {
        $userId = $userService->registerUser($userData);
        echo "注册成功,用户ID: $userId\n";
    } catch (UserRegistrationException $e) {
        echo "注册失败: " . $e->getMessage() . "\n";
        foreach ($e->getErrors() as $field => $error) {
            echo "  $field: $error\n";
        }
    }
}
?>

2. 文件操作异常处理

<?php
class FileOperationException extends Exception {
    private $filename;

    public function __construct($filename, $message, $code = 0, Exception $previous = null) {
        $this->filename = $filename;
        parent::__construct($message, $code, $previous);
    }

    public function getFilename() {
        return $this->filename;
    }
}

class FileManager {
    private $basePath;

    public function __construct($basePath = './files') {
        $this->basePath = $basePath;
        $this->ensureDirectoryExists($basePath);
    }

    public function writeFile($filename, $content) {
        $fullPath = $this->basePath . '/' . $filename;

        try {
            // 检查目录是否可写
            $this->checkDirectoryWritable(dirname($fullPath));

            // 写入文件
            $bytesWritten = file_put_contents($fullPath, $content, LOCK_EX);

            if ($bytesWritten === false) {
                throw new FileOperationException($filename, "文件写入失败");
            }

            echo "文件写入成功: $filename ($bytesWritten 字节)\n";
            return true;

        } catch (Exception $e) {
            if (!($e instanceof FileOperationException)) {
                throw new FileOperationException($filename, "文件操作异常", 0, $e);
            }
            throw $e;
        }
    }

    public function readFile($filename) {
        $fullPath = $this->basePath . '/' . $filename;

        try {
            // 检查文件是否存在
            if (!file_exists($fullPath)) {
                throw new FileOperationException($filename, "文件不存在");
            }

            // 检查文件是否可读
            if (!is_readable($fullPath)) {
                throw new FileOperationException($filename, "文件不可读");
            }

            $content = file_get_contents($fullPath);

            if ($content === false) {
                throw new FileOperationException($filename, "文件读取失败");
            }

            echo "文件读取成功: $filename\n";
            return $content;

        } catch (Exception $e) {
            if (!($e instanceof FileOperationException)) {
                throw new FileOperationException($filename, "文件操作异常", 0, $e);
            }
            throw $e;
        }
    }

    public function copyFile($sourceFile, $destinationFile) {
        try {
            $content = $this->readFile($sourceFile);
            $this->writeFile($destinationFile, $content);
            echo "文件复制成功: $sourceFile -> $destinationFile\n";
        } catch (FileOperationException $e) {
            throw new FileOperationException(
                "$sourceFile -> $destinationFile",
                "文件复制失败: " . $e->getMessage(),
                0,
                $e
            );
        }
    }

    private function ensureDirectoryExists($directory) {
        if (!is_dir($directory)) {
            if (!mkdir($directory, 0755, true)) {
                throw new FileOperationException($directory, "目录创建失败");
            }
        }
    }

    private function checkDirectoryWritable($directory) {
        if (!is_writable($directory)) {
            throw new FileOperationException($directory, "目录不可写");
        }
    }
}

// 使用示例
try {
    $fileManager = new FileManager();

    // 写入文件
    $fileManager->writeFile('test.txt', 'Hello, World!');

    // 读取文件
    $content = $fileManager->readFile('test.txt');
    echo "文件内容: $content\n";

    // 复制文件
    $fileManager->copyFile('test.txt', 'backup.txt');

    // 测试错误情况
    $fileManager->readFile('nonexistent.txt');

} catch (FileOperationException $e) {
    echo "文件操作错误: " . $e->getMessage() . "\n";
    echo "文件名: " . $e->getFilename() . "\n";

    // 显示异常链
    $previous = $e->getPrevious();
    if ($previous) {
        echo "原始错误: " . $previous->getMessage() . "\n";
    }
}
?>

3. API响应异常处理

<?php
class APIException extends Exception {
    private $httpCode;
    private $response;

    public function __construct($message, $httpCode = 500, $response = null, $code = 0, Exception $previous = null) {
        $this->httpCode = $httpCode;
        $this->response = $response;
        parent::__construct($message, $code, $previous);
    }

    public function getHttpCode() {
        return $this->httpCode;
    }

    public function getResponse() {
        return $this->response;
    }
}

class APIClient {
    private $baseURL;
    private $timeout;

    public function __construct($baseURL, $timeout = 30) {
        $this->baseURL = rtrim($baseURL, '/');
        $this->timeout = $timeout;
    }

    public function request($method, $endpoint, $data = []) {
        $url = $this->baseURL . '/' . ltrim($endpoint, '/');

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        ]);

        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new APIException("网络请求失败: $error", 0);
        }

        if ($httpCode >= 400) {
            $responseData = json_decode($response, true);
            $message = $responseData['message'] ?? "API请求失败";
            throw new APIException($message, $httpCode, $responseData);
        }

        return json_decode($response, true);
    }

    public function get($endpoint) {
        return $this->request('GET', $endpoint);
    }

    public function post($endpoint, $data) {
        return $this->request('POST', $endpoint, $data);
    }

    public function put($endpoint, $data) {
        return $this->request('PUT', $endpoint, $data);
    }

    public function delete($endpoint) {
        return $this->request('DELETE', $endpoint);
    }
}

class UserServiceAPI {
    private $client;

    public function __construct(APIClient $client) {
        $this->client = $client;
    }

    public function getUser($id) {
        try {
            return $this->client->get("users/$id");
        } catch (APIException $e) {
            if ($e->getHttpCode() === 404) {
                throw new APIException("用户不存在", 404);
            }
            throw $e;
        }
    }

    public function createUser($userData) {
        try {
            return $this->client->post('users', $userData);
        } catch (APIException $e) {
            if ($e->getHttpCode() === 422) {
                throw new APIException("用户数据验证失败: " . $e->getMessage(), 422);
            }
            throw $e;
        }
    }

    public function updateUser($id, $userData) {
        try {
            return $this->client->put("users/$id", $userData);
        } catch (APIException $e) {
            if ($e->getHttpCode() === 404) {
                throw new APIException("用户不存在,无法更新", 404);
            }
            throw $e;
        }
    }

    public function deleteUser($id) {
        try {
            return $this->client->delete("users/$id");
        } catch (APIException $e) {
            if ($e->getHttpCode() === 404) {
                throw new APIException("用户不存在,无法删除", 404);
            }
            throw $e;
        }
    }
}

// 模拟使用
function simulateAPI() {
    echo "=== API调用演示 ===\n";

    // 模拟API客户端
    $client = new APIClient('https://api.example.com', 10);
    $userService = new UserServiceAPI($client);

    // 模拟各种API调用
    $operations = [
        ['method' => 'getUser', 'params' => [1], 'expectError' => false],
        ['method' => 'getUser', 'params' => [999], 'expectError' => true],  // 用户不存在
        ['method' => 'createUser', 'params' => [['name' => '']], 'expectError' => true],  // 验证失败
        ['method' => 'updateUser', 'params' => [999, ['name' => 'Test']], 'expectError' => true],  // 用户不存在
    ];

    foreach ($operations as $index => $operation) {
        echo "\n--- 操作 " . ($index + 1) . " ---\n";
        try {
            $result = $userService->{$operation['method']}(...$operation['params']);
            echo "操作成功\n";
            if ($result) {
                echo "结果: " . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
            }
        } catch (APIException $e) {
            echo "API异常: " . $e->getMessage() . "\n";
            echo "HTTP状态码: " . $e->getHttpCode() . "\n";
            if ($e->getResponse()) {
                echo "响应数据: " . json_encode($e->getResponse(), JSON_UNESCAPED_UNICODE) . "\n";
            }
        }
    }
}

simulateAPI();
?>

异常处理的最佳实践

1. 异常处理的时机

<?php
// 好的做法:对可恢复的错误使用异常
function processPayment($amount) {
    if ($amount <= 0) {
        throw new InvalidArgumentException("金额必须大于0");
    }

    if ($amount > 10000) {
        throw new RuntimeException("单笔交易金额不能超过10000");
    }

    // 处理支付逻辑
    return processPaymentLogic($amount);
}

// 不好的做法:对预期内的错误使用异常
function getUser($id) {
    if ($id <= 0) {
        throw new InvalidArgumentException("ID必须大于0");  // 不如直接返回false
    }

    $user = findUserInDatabase($id);
    if (!$user) {
        throw new RuntimeException("用户不存在");  // 不如返回null
    }

    return $user;
}

// 更好的做法
function getUser($id) {
    if ($id <= 0) {
        return false;
    }

    return findUserInDatabase($id) ?: null;
}
?>

2. 异常粒度控制

<?php
// 过细的异常粒度(不推荐)
class TooShortNameException extends Exception {}
class TooLongNameException extends Exception {}
class InvalidCharacterException extends Exception {}

// 合适的异常粒度(推荐)
class ValidationException extends Exception {
    private $field;
    private $value;

    public function __construct($field, $value, $message, $code = 0) {
        $this->field = $field;
        $this->value = $value;
        parent::__construct($message, $code);
    }

    public function getField() { return $this->field; }
    public function getValue() { return $this->value; }
}

function validateName($name) {
    if (strlen($name) < 2) {
        throw new ValidationException('name', $name, '姓名至少2个字符');
    }
    if (strlen($name) > 50) {
        throw new ValidationException('name', $name, '姓名不能超过50个字符');
    }
    if (!preg_match('/^[a-zA-Z\s]+$/', $name)) {
        throw new ValidationException('name', $name, '姓名只能包含字母和空格');
    }
    return true;
}
?>

3. 异常信息规范

<?php
// 好的做法:提供清晰的异常信息
class DatabaseException extends Exception {
    public function __construct($operation, $table, $message, $code = 0, Exception $previous = null) {
        $fullMessage = "数据库操作失败 [$operation] 表[$table]: $message";
        parent::__construct($fullMessage, $code, $previous);
    }
}

// 使用
try {
    executeQuery("SELECT", "users", "WHERE id = ?");
} catch (DatabaseException $e) {
    error_log($e->getMessage());  // 清晰的错误日志
    showUserError("系统暂时繁忙,请稍后再试");  // 用户友好的提示
}

// 异常信息应该包含:
// 1. 发生了什么
// 2. 在哪里发生
// 3. 为什么发生
// 4. 如何修复(如果可能)
?>

4. 异常与日志记录

<?php
class ExceptionLogger {
    private $logFile;

    public function __construct($logFile = 'exceptions.log') {
        $this->logFile = $logFile;
    }

    public function logException(Exception $e) {
        $timestamp = date('Y-m-d H:i:s');
        $context = $this->getContext();

        $logEntry = sprintf(
            "[%s] %s: %s\n文件: %s (%d)\n请求: %s\nIP: %s\n堆栈跟踪:\n%s\n---\n",
            $timestamp,
            get_class($e),
            $e->getMessage(),
            $e->getFile(),
            $e->getLine(),
            $context['request'] ?? 'CLI',
            $context['ip'] ?? '127.0.0.1',
            $e->getTraceAsString()
        );

        file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
    }

    private function getContext() {
        $context = [];

        if (isset($_SERVER['REQUEST_URI'])) {
            $context['request'] = $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'];
        }

        if (isset($_SERVER['REMOTE_ADDR'])) {
            $context['ip'] = $_SERVER['REMOTE_ADDR'];
        }

        if (isset($_SERVER['HTTP_USER_AGENT'])) {
            $context['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        }

        return $context;
    }
}

// 全局异常处理器
function globalExceptionHandler($exception) {
    $logger = new ExceptionLogger();
    $logger->logException($exception);

    // 显示友好的错误页面
    http_response_code(500);
    include 'error_pages/500.html';
}

// 设置全局异常处理器
set_exception_handler('globalExceptionHandler');

// 使用示例
throw new Exception("这是一个测试异常");
?>

异常处理的性能考虑

异常处理的开销

<?php
// 测试异常处理的性能
function testWithException($iterations = 1000) {
    $start = microtime(true);

    for ($i = 0; $i < $iterations; $i++) {
        try {
            if ($i % 2 == 0) {
                throw new Exception("测试异常");
            }
        } catch (Exception $e) {
            // 处理异常
        }
    }

    $end = microtime(true);
    return $end - $start;
}

function testWithoutException($iterations = 1000) {
    $start = microtime(true);

    for ($i = 0; $i < $iterations; $i++) {
        if ($i % 2 == 0) {
            // 使用错误码而不是异常
            $result = ['error' => '测试错误'];
        }
    }

    $end = microtime(true);
    return $end - $start;
}

echo "异常处理时间: " . testWithException() . " 秒\n";
echo "错误码处理时间: " . testWithoutException() . " 秒\n";

// 建议:
// 1. 不要在性能敏感的代码路径中使用异常
// 2. 异常应该用于真正的异常情况,而不是控制流
// 3. 对于预期的错误,考虑使用返回值
?>

通过本节的学习,你应该掌握了PHP异常处理机制的各个方面,包括基本语法、自定义异常、嵌套处理、实际应用和最佳实践。异常处理是构建健壮、可维护应用程序的重要组成部分,下一节我们将学习PHP调试技巧。