错误类型

什么是错误

在PHP程序运行过程中,可能会出现各种各样的问题,这些问题我们统称为"错误"。理解不同类型的错误对于编写健壮的PHP应用程序至关重要。

错误是指程序在执行过程中遇到的问题,这些问题可能导致程序无法正常执行或产生意外的结果。

PHP错误级别

PHP定义了多种错误级别,每种级别代表了不同严重程度的问题:

1. 致命错误(Fatal Errors)

致命错误会导致程序立即终止执行,无法继续运行。

<?php
// 致命错误示例1:调用不存在的函数
echo "程序开始执行\n";
$result = undefined_function(); // 致命错误:函数不存在
echo "这行不会执行\n";  // 这行永远不会执行
?>

<?php
// 致命错误示例2:访问不存在的类
$obj = new NonExistentClass(); // 致命错误:类不存在
?>

<?php
// 致命错误示例3:包含不存在的文件(require)
require 'non_existent_file.php'; // 致命错误,程序终止
echo "程序继续"; // 不会执行
?>

2. 警告错误(Warnings)

警告错误不会终止程序执行,但提示程序可能存在问题。

<?php
// 警告示例1:包含不存在的文件(include)
echo "程序开始执行\n";
include 'non_existent_file.php'; // 警告:文件不存在,但程序继续
echo "程序继续执行\n"; // 这行会执行
?>

<?php
// 警告示例2:错误的参数类型
echo strlen(123); // 警告:期望字符串,但传入了整数
echo "程序继续\n"; // 程序继续执行
?>

<?php
// 警告示例3:使用未定义的变量
echo $undefined_variable; // 警告:变量未定义
echo "程序继续\n";
?>

3. 通知错误(Notices)

通知错误表示程序存在轻微问题,通常不会影响程序执行。

<?php
// 通知示例1:使用未定义的数组索引
$array = ['a' => 1];
echo $array['b']; // 通知:索引 'b' 不存在
echo "程序继续\n";
?>

<?php
// 通知示例2:除以零
echo 10 / 0; // 通知:除以零,返回INF或false
echo "程序继续\n";
?>

4. 弃用错误(Deprecated)

弃用错误表示使用了在未来版本中可能会被移除的功能。

<?php
// 弃用示例:使用已弃用的函数
$result = ereg("pattern", "string"); // 弃用:ereg函数在PHP 7.0+中被移除

// 弃用示例:使用已弃用的语法
$object = &new stdClass(); // 弃用:引用语法已弃用
?>

错误报告级别

PHP提供了常量来表示不同的错误级别:

<?php
// 常用错误级别常量
echo E_ERROR;           // 1  - 致命错误
echo E_WARNING;         // 2  - 警告
echo E_PARSE;           // 4  - 编译时解析错误
echo E_NOTICE;          // 8  - 通知
echo E_CORE_ERROR;      // 16 - PHP启动时的致命错误
echo E_CORE_WARNING;    // 32 - PHP启动时的警告
echo E_COMPILE_ERROR;   // 64 - 编译时的致命错误
echo E_COMPILE_WARNING; // 128- 编译时的警告
echo E_USER_ERROR;      // 256- 用户定义的致命错误
echo E_USER_WARNING;    // 512- 用户定义的警告
echo E_USER_NOTICE;     // 1024- 用户定义的通知
echo E_STRICT;          // 2048- 严格标准化建议
echo E_RECOVERABLE_ERROR; // 4096- 可捕获的致命错误
echo E_DEPRECATED;      // 8192- 弃用警告
echo E_USER_DEPRECATED; // 16384- 用户定义的弃用警告
echo E_ALL;             // 32767- 所有错误
?>

配置错误报告

在php.ini中配置

; 错误报告设置
error_reporting = E_ALL & ~E_DEPRECATED
display_errors = On          ; 开发环境显示错误
display_errors = Off         ; 生产环境不显示错误
log_errors = On              ; 记录错误到日志
error_log = /var/log/php_errors.log  ; 错误日志文件路径

; 常见配置组合
; 开发环境
error_reporting = E_ALL
display_errors = On
log_errors = On

; 生产环境
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
log_errors = On

在代码中配置

<?php
// 设置错误报告级别
error_reporting(E_ALL);                 // 报告所有错误
error_reporting(E_ALL & ~E_NOTICE);     // 报告除通知外的所有错误
error_reporting(0);                     // 关闭所有错误报告

// 显示错误
ini_set('display_errors', 1);          // 开启错误显示
ini_set('display_errors', 0);          // 关闭错误显示

// 记录错误
ini_set('log_errors', 1);              // 开启错误日志
ini_set('error_log', 'my_errors.log'); // 设置错误日志文件

// 动态配置示例
function setEnvironmentErrorReporting($environment) {
    switch ($environment) {
        case 'development':
            error_reporting(E_ALL);
            ini_set('display_errors', 1);
            ini_set('log_errors', 1);
            break;
        case 'testing':
            error_reporting(E_ALL & ~E_DEPRECATED);
            ini_set('display_errors', 1);
            ini_set('log_errors', 1);
            break;
        case 'production':
            error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
            ini_set('display_errors', 0);
            ini_set('log_errors', 1);
            break;
    }
}

// 使用示例
setEnvironmentErrorReporting('development');
?>

常见错误类型详解

1. 语法错误(Parse Errors)

语法错误是最基础的错误类型,通常在代码执行前就会被检测到。

<?php
// 缺少分号
echo "Hello World"  // Parse error: syntax error, unexpected 'echo'

// 缺少引号
echo "Hello World;  // Parse error: syntax error, unexpected end of file

// 括号不匹配
if ($condition {  // Parse error: syntax error, unexpected '{'
    echo "condition true";
}

// 错误的类定义
class MyClass {     // Parse error: syntax error, unexpected end of file
    public $property

    public function method() {
        echo "method";
    }
}
?>

2. 逻辑错误(Logical Errors)

逻辑错误不会产生错误信息,但程序的行为不符合预期。

<?php
// 逻辑错误示例1:条件判断错误
function isAdult($age) {
    return $age > 18;  // 错误:应该是 >= 18
}

// 逻辑错误示例2:循环条件错误
function printNumbers($n) {
    for ($i = 0; $i <= $n; $i++) {  // 错误:应该是 < $n
        echo $i . " ";
    }
}

// 逻辑错误示例3:数组索引错误
function getFirstElement($array) {
    return $array[1];  // 错误:应该是 $array[0]
}

// 逻辑错误示例4:字符串连接错误
function createFullName($firstName, $lastName) {
    return $firstName + $lastName;  // 错误:应该使用 . 连接
}

// 调试逻辑错误的方法
function safeDivide($a, $b) {
    if ($b == 0) {
        echo "错误:除数不能为零\n";
        return false;
    }
    return $a / $b;
}

// 使用断言来检测逻辑错误
function calculateDiscount($amount, $discount) {
    assert($amount >= 0, "金额必须大于等于0");
    assert($discount >= 0 && $discount <= 100, "折扣必须在0-100之间");

    return $amount * ($discount / 100);
}
?>

3. 运行时错误(Runtime Errors)

运行时错误在程序执行过程中发生。

<?php
// 运行时错误示例1:除以零
function divide($a, $b) {
    if ($b == 0) {
        trigger_error("除数不能为零", E_USER_WARNING);
        return false;
    }
    return $a / $b;
}

// 运行时错误示例2:内存不足
function processLargeData() {
    // 尝试分配过多内存
    $largeArray = array_fill(0, 10000000, "large string");
}

// 运行时错误示例3:文件操作错误
function readFile($filename) {
    $handle = fopen($filename, 'r');
    if (!$handle) {
        trigger_error("无法打开文件: $filename", E_USER_WARNING);
        return false;
    }

    $content = fread($handle, filesize($filename));
    fclose($handle);
    return $content;
}

// 运行时错误示例4:数据库连接错误
function connectToDatabase() {
    $conn = mysqli_connect("invalid_host", "user", "password", "database");
    if (!$conn) {
        trigger_error("数据库连接失败: " . mysqli_connect_error(), E_USER_ERROR);
        return false;
    }
    return $conn;
}
?>

错误处理函数

trigger_error() - 触发用户定义的错误

<?php
function validateEmail($email) {
    if (empty($email)) {
        trigger_error("邮箱地址不能为空", E_USER_WARNING);
        return false;
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        trigger_error("邮箱地址格式不正确: $email", E_USER_WARNING);
        return false;
    }

    return true;
}

function processUserRegistration($userData) {
    // 验证必填字段
    if (!isset($userData['name'])) {
        trigger_error("用户名是必填字段", E_USER_ERROR);
    }

    if (!isset($userData['email'])) {
        trigger_error("邮箱是必填字段", E_USER_ERROR);
    }

    // 验证邮箱格式
    if (!validateEmail($userData['email'])) {
        trigger_error("注册失败:邮箱验证错误", E_USER_NOTICE);
        return false;
    }

    echo "用户注册成功\n";
    return true;
}

// 使用示例
$userData = [
    'name' => '',
    'email' => 'invalid-email'
];

processUserRegistration($userData);
?>

set_error_handler() - 自定义错误处理

<?php
// 自定义错误处理函数
function customErrorHandler($errno, $errstr, $errfile, $errline) {
    $errorType = [
        E_ERROR => 'Error',
        E_WARNING => 'Warning',
        E_PARSE => 'Parse Error',
        E_NOTICE => 'Notice',
        E_USER_ERROR => 'User Error',
        E_USER_WARNING => 'User Warning',
        E_USER_NOTICE => 'User Notice'
    ];

    $type = isset($errorType[$errno]) ? $errorType[$errno] : 'Unknown';

    $message = sprintf(
        "[%s] %s: %s in %s on line %d\n",
        date('Y-m-d H:i:s'),
        $type,
        $errstr,
        $errfile,
        $errline
    );

    // 记录到日志文件
    file_put_contents('error.log', $message, FILE_APPEND);

    // 根据错误类型决定是否显示错误
    if (in_array($errno, [E_ERROR, E_USER_ERROR])) {
        echo "系统发生错误,请联系管理员";
    } else {
        echo $message;  // 开发环境显示详细错误
    }

    // 返回true表示已处理错误,不再使用PHP默认处理
    return true;
}

// 设置自定义错误处理函数
set_error_handler('customErrorHandler');

// 测试错误处理
echo $undefinedVariable;  // 触发通知错误
echo 10 / 0;              // 触发警告错误
trigger_error("测试用户错误", E_USER_ERROR);
?>

restore_error_handler() - 恢复默认错误处理

<?php
function temporaryErrorHandler($errno, $errstr, $errfile, $errline) {
    echo "临时错误处理器: $errstr\n";
    return true;
}

// 设置临时错误处理器
set_error_handler('temporaryErrorHandler');

echo $undefinedVar;  // 使用临时错误处理器

// 恢复默认错误处理器
restore_error_handler();

echo $anotherUndefinedVar;  // 使用默认错误处理器
?>

错误日志记录

记录到文件

<?php
function logError($message, $level = 'ERROR') {
    $timestamp = date('Y-m-d H:i:s');
    $logMessage = "[$timestamp] [$level] $message\n";

    // 确保日志目录存在
    $logDir = 'logs';
    if (!is_dir($logDir)) {
        mkdir($logDir, 0755, true);
    }

    // 写入日志文件
    $logFile = $logDir . '/app_' . date('Y-m-d') . '.log';
    file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
}

function logErrorWithContext($errno, $errstr, $errfile, $errline) {
    $context = [
        'timestamp' => date('Y-m-d H:i:s'),
        'level' => $errno,
        'message' => $errstr,
        'file' => $errfile,
        'line' => $errline,
        'url' => $_SERVER['REQUEST_URI'] ?? 'CLI',
        'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
        'ip' => $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
    ];

    $logMessage = json_encode($context, JSON_UNESCAPED_UNICODE) . "\n";
    file_put_contents('logs/context_errors.log', $logMessage, FILE_APPEND);
}

// 使用示例
logError("用户登录失败", "WARNING");
logError("数据库连接超时", "ERROR");

// 设置错误处理器记录详细错误
set_error_handler('logErrorWithContext');
?>

错误日志轮转

<?php
class ErrorLogger {
    private $logDir;
    private $maxFileSize;
    private $maxFiles;

    public function __construct($logDir = 'logs', $maxFileSize = 10485760, $maxFiles = 10) {
        $this->logDir = $logDir;
        $this->maxFileSize = $maxFileSize;  // 10MB
        $this->maxFiles = $maxFiles;

        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0755, true);
        }
    }

    public function log($message, $level = 'INFO') {
        $timestamp = date('Y-m-d H:i:s');
        $logEntry = "[$timestamp] [$level] $message\n";

        $currentLogFile = $this->getCurrentLogFile();

        // 检查文件大小,如果超过限制则轮转
        if (file_exists($currentLogFile) && filesize($currentLogFile) > $this->maxFileSize) {
            $this->rotateLog();
        }

        file_put_contents($currentLogFile, $logEntry, FILE_APPEND | LOCK_EX);
    }

    private function getCurrentLogFile() {
        return $this->logDir . '/app.log';
    }

    private function rotateLog() {
        $currentLogFile = $this->getCurrentLogFile();

        // 移动现有的日志文件
        for ($i = $this->maxFiles - 1; $i > 0; $i--) {
            $oldFile = $currentLogFile . '.' . $i;
            $newFile = $currentLogFile . '.' . ($i + 1);

            if (file_exists($oldFile)) {
                if ($i == $this->maxFiles - 1) {
                    unlink($oldFile);  // 删除最旧的文件
                } else {
                    rename($oldFile, $newFile);
                }
            }
        }

        // 重命名当前日志文件
        if (file_exists($currentLogFile)) {
            rename($currentLogFile, $currentLogFile . '.1');
        }
    }

    public function cleanOldLogs($days = 30) {
        $files = glob($this->logDir . '/*.log*');
        $cutoffTime = time() - ($days * 24 * 60 * 60);

        foreach ($files as $file) {
            if (filemtime($file) < $cutoffTime) {
                unlink($file);
            }
        }
    }
}

// 使用示例
$logger = new ErrorLogger('logs', 1048576, 5);  // 1MB,最多5个文件
$logger->log("应用程序启动", "INFO");
$logger->log("用户登录", "INFO");
$logger->log("处理请求", "DEBUG");
?>

开发环境 vs 生产环境

开发环境配置

<?php
function setDevelopmentEnvironment() {
    // 显示所有错误
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);

    // 设置详细的错误报告
    ini_set('log_errors', 1);
    ini_set('error_log', 'dev_errors.log');

    // 启用调试信息
    ini_set('html_errors', 1);
    ini_set('docref_root', 'http://php.net/manual/en/');

    echo "开发环境错误处理已启用\n";
}

// 开发环境的错误处理器
function developmentErrorHandler($errno, $errstr, $errfile, $errline) {
    echo "<div style='background: #ffeeee; border: 1px solid #ff0000; padding: 10px; margin: 10px;'>";
    echo "<strong>错误:</strong> $errstr<br>";
    echo "<strong>文件:</strong> $errfile<br>";
    echo "<strong>行号:</strong> $errline<br>";

    // 显示调用栈
    echo "<strong>调用栈:</strong><br>";
    debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);

    echo "</div>";
    return true;
}

setDevelopmentEnvironment();
set_error_handler('developmentErrorHandler');
?>

生产环境配置

<?php
function setProductionEnvironment() {
    // 不显示错误给用户
    error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
    ini_set('display_errors', 0);
    ini_set('display_startup_errors', 0);

    // 记录所有错误
    ini_set('log_errors', 1);
    ini_set('error_log', '/var/log/php/production_errors.log');

    // 设置错误日志格式
    ini_set('log_errors_max_len', 1024);

    echo "生产环境错误处理已配置\n";
}

// 生产环境的错误处理器
function productionErrorHandler($errno, $errstr, $errfile, $errline) {
    // 记录错误到文件
    $logEntry = sprintf(
        "[%s] [%d] %s in %s on line %d [URL: %s] [IP: %s]",
        date('Y-m-d H:i:s'),
        $errno,
        $errstr,
        $errfile,
        $errline,
        $_SERVER['REQUEST_URI'] ?? 'CLI',
        $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
    );

    file_put_contents('production_errors.log', $logEntry . PHP_EOL, FILE_APPEND);

    // 发送邮件通知管理员(仅对严重错误)
    if ($errno === E_ERROR || $errno === E_USER_ERROR) {
        error_log("严重错误: $logEntry", 1, 'admin@example.com');
    }

    // 显示友好的错误页面给用户
    if (!headers_sent()) {
        header('HTTP/1.1 500 Internal Server Error');
        include 'error_pages/500.html';
    }

    return true;
}

// 设置环境
setProductionEnvironment();
set_error_handler('productionErrorHandler');
?>

常见错误排查技巧

1. 启用详细错误报告

<?php
// 临时启用所有错误报告
ini_set('display_errors', 1);
error_reporting(E_ALL);

// 或者使用
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(-1);  // -1 表示所有错误
?>

2. 检查语法错误

<?php
// 使用命令行检查语法
// php -l filename.php

// 在代码中检查语法
function checkSyntax($code) {
    $tempFile = tempnam(sys_get_temp_dir(), 'syntax_check');
    file_put_contents($tempFile, "<?php\n" . $code);

    $output = [];
    $returnCode = 0;
    exec("php -l $tempFile 2>&1", $output, $returnCode);

    unlink($tempFile);

    return $returnCode === 0;
}

// 使用示例
$code = 'echo "Hello World"';  // 缺少分号
if (!checkSyntax($code)) {
    echo "代码存在语法错误\n";
}
?>

3. 使用var_dump()调试

<?php
function debugVariable($var, $label = '') {
    echo "<pre>";
    if ($label) {
        echo "$label: ";
    }
    var_dump($var);
    echo "</pre>";
}

function debugArray($array) {
    echo "<pre>";
    print_r($array);
    echo "</pre>";
}

// 使用示例
$user = ['name' => 'John', 'age' => 30, 'email' => 'john@example.com'];
debugVariable($user, '用户信息');
debugArray($user);
?>

错误预防策略

1. 输入验证

<?php
function validateInput($data, $rules) {
    $errors = [];

    foreach ($rules as $field => $rule) {
        $value = $data[$field] ?? null;

        // 必填验证
        if (isset($rule['required']) && $rule['required'] && empty($value)) {
            $errors[$field] = "$field 是必填字段";
            continue;
        }

        // 类型验证
        if (isset($rule['type']) && $value !== null) {
            switch ($rule['type']) {
                case 'email':
                    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                        $errors[$field] = "$field 必须是有效的邮箱地址";
                    }
                    break;
                case 'int':
                    if (!filter_var($value, FILTER_VALIDATE_INT)) {
                        $errors[$field] = "$field 必须是整数";
                    }
                    break;
                case 'string':
                    if (!is_string($value)) {
                        $errors[$field] = "$field 必须是字符串";
                    }
                    break;
            }
        }

        // 长度验证
        if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) {
            $errors[$field] = "$field 长度不能少于 {$rule['min_length']} 个字符";
        }

        if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) {
            $errors[$field] = "$field 长度不能超过 {$rule['max_length']} 个字符";
        }
    }

    return $errors;
}

// 使用示例
$userData = [
    'name' => 'Jo',
    'email' => 'invalid-email',
    'age' => 'not_a_number'
];

$rules = [
    'name' => [
        'required' => true,
        'type' => 'string',
        'min_length' => 2,
        'max_length' => 50
    ],
    'email' => [
        'required' => true,
        'type' => 'email'
    ],
    'age' => [
        'type' => 'int'
    ]
];

$errors = validateInput($userData, $rules);
if (!empty($errors)) {
    echo "验证失败:\n";
    print_r($errors);
}
?>

2. 使用isset()检查变量

<?php
// 安全的数组访问
function safeArrayAccess($array, $key, $default = null) {
    return isset($array[$key]) ? $array[$key] : $default;
}

// 安全的对象属性访问
function safePropertyAccess($object, $property, $default = null) {
    return isset($object->$property) ? $object->$property : $default;
}

// 使用示例
$config = [
    'database' => [
        'host' => 'localhost',
        'port' => 3306
    ]
];

$dbHost = safeArrayAccess($config, 'database')['host'] ?? 'default_host';
$dbPort = safeArrayAccess($config, 'database', ['port' => 'default_port'])['port'];
$unknown = safeArrayAccess($config, 'unknown', 'default_value');
?>

3. 使用三元运算符和null合并操作符

<?php
// 传统方式
if (isset($_GET['page'])) {
    $page = $_GET['page'];
} else {
    $page = 1;
}

// 使用三元运算符
$page = isset($_GET['page']) ? $_GET['page'] : 1;

// 使用null合并操作符(PHP 7+)
$page = $_GET['page'] ?? 1;

// 链式null合并操作符
$user = $_GET['user'] ?? $_SESSION['user'] ?? $defaultUser;

// 安全的数组访问
$config = $config['database']['host'] ?? 'localhost';
?>

通过本节的学习,你应该了解了PHP中不同类型的错误、如何配置错误报告、以及如何处理和预防错误。理解这些概念对于编写健壮、可靠的PHP应用程序至关重要。下一节我们将学习更高级的异常处理机制。