PHP 错误和异常处理(下)

上篇我们讲了 PHP 中的错误报告和捕获,今天,我们来看看 PHP 程序中的异常处理。

错误 vs. 异常

错误与异常可以看作一对孪生兄弟,从严格的面向对象编程角度来说,错误指的是致命错误(Fatal Error,比如编译错误和语法错误),出现运行时错误后,程序应该无法继续往后执行,需要执行一些清理工作并记录日志后退出当前处理流程。

而异常指的是程序中出现的可预测的、可恢复的中轻度问题,比如数空对象引用、文件不存在、除数为零、数组越界等,当程序运行时出现异常后,我们可以对其进行捕获,或者抛给上层的业务代码处理,和错误报告类似,如果通过 set_exception_hanlder 函数定义了全局异常处理器,则所有未处理异常会集中到这里处理,如果没有定义任何处理异常的代码,最终会抛出一个 Fatal Error(也就是说,所有未处理异常都会被当作错误进行兜底处理)。程序出现异常后,应该可以继续往后执行。

但是我们在 PHP 中可以看到两者的边界并不明显,因为异常是 PHP 5 之后实现完整面向对象机制后引入的,之前的 PHP 中只有错误,没有异常,所以你可以看到那么多的错误级别,比如 Notice、Warning、Deprecated 这些中轻度错误,实际上完全可以通过异常进行处理。

层次结构

在 PHP 7 中,所有错误都归属于 Error 类,所有异常都归属于 Exception 类,两者是并列关系,并且最新 PHP 内置错误和异常类型如下表所示:

-w668

ErrorException 类又都实现了 Throwable 接口。

异常处理

有了以上的了解,大家应该大体上明白了异常是怎么回事以及所处的位置,接下来,我们来看看如何处理异常,我们按照三个层级递进:首先是在定义代码的地方捕获并处理,然后是在上层调用的地方捕获并处理,以及定义全局异常处理器处理。

php_learning/oop 目录下新建 exception.php 保存本篇教程的代码。

捕获异常

首先来看如何在代码定义的地方捕获异常,和错误捕获一样,我们可以 try...catch... 语句块捕获异常。

exception.php 中编写一段测试代码:

-w961

我们试图从 $book 数组中访问一个不存在的索引,此时没有定义任何异常捕获和处理逻辑,所以会以错误报告方式进行兜底处理:

-w938

现在我们在 getItemFromBook 方法中会参数进行验证,如果不满足要求则抛出异常:

<?php
function getItemFromBook($book, $key)
{
    if (empty($book) || !key_exists($key, $book)) {
        throw new InvalidArgumentException("数组为空或者对应索引不存在!");
    }
    return $book[$key];
}

通过 throw 关键字即可抛出异常,这里我们通过 new 关键字实例化了一个内置的 InvalidArgumentException 异常对象作为返回值抛出。

抛出异常后会终止后续代码的执行,然后我们可以在调用的地方通过 try/catch 对这个异常进行捕获:

try {
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage();
    exit();
}
var_dump($val);

其原理是当 try 语句块中遇到异常后,会通过 catch 语句进行捕获,如果抛出的异常和声明异常类型匹配,则执行 catch 语句块中的内容。这样,当我们再次执行代码时,就会捕获这个异常:

-w542

如果你不知道抛出的异常类型是什么,可以通过 Exception 基类捕获(或者其他父级异常类),也就是说,此处也符合父子类型的转化逻辑:

try {
    $val = getItemFromBook($book, 'desc');
} catch (Exception $exception) {
    echo $exception->getMessage();
    exit();
}
var_dump($val);

但是如果不是 InvalidArgumentException 或者其父类,就不能捕获了:

try {
    $val = getItemFromBook($book, 'desc');
} catch (RuntimeException $exception) {
    echo $exception->getMessage();
    exit();
}
var_dump($val);

执行上述代码,打印结果如下:

-w1073

未处理异常会转化为 Fatal Error 处理。

如果调用程序抛出了多个异常:

function getItemFromBook($book, $key)
{
    if (empty($book)) {
        throw new InvalidArgumentException("数组为空!");
    }
    if (!key_exists($key, $book)) {
        throw new OutOfBoundsException("对应索引不存在!");
    }
    return $book[$key];
}

可以通过多个 catch 语句进行捕获:

try {
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage();
    exit();
} catch (OutOfBoundsException $exception) {
    echo $exception->getMessage();
    exit();
}
var_dump($val);

但是由于我们在每个 catch 分支里面都调用 exit() 退出程序,可以通过添加 finally 语句块定义一个兜底逻辑:

$exit = false;
try {
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
    $exit = true;
} catch (OutOfBoundsException $exception) {
    echo $exception->getMessage() . PHP_EOL;
    $exit = true;
} finally {
    $exit ? exit() : var_dump($val);
}

不管 try 语句块中的代码是否抛出异常,finally 语句块中的代码都会执行,如果抛出异常,则会先执行 catch 语句块中的代码,再执行 finally 语句块中的代码,否则会直接执行 finally 语句块中的代码。

抛出异常

我们也可以在捕获到异常后不进行处理,直接抛出,交给上一层调用代码进行进一步处理:

try {
    $val = getItemFromBook([], null);
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    throw $exception;
} catch (OutOfBoundsException $exception) {
    throw $exception;
} finally {
    var_dump($val);
}

上一层的处理逻辑也无非是进行 try...catch... 捕获后进行处理或者继续抛出。

全局异常处理器

在进行系统框架设计时,考虑到系统的稳健型,总会有一些异常的「漏网之鱼」没有被捕获和处理,这个时候就要通过 set_exception_handler 函数注册全局的异常处理器来处理这些未被捕获和处理的异常:

<?php
...

function myExceptionHandler(Exception $exception)
{
    echo 'Uncaught Exception [' . get_class($exception) . ']: ' . $exception->getMessage() . PHP_EOL;
    echo 'Thrown in ' . $exception->getFile() . ' on line ' . $exception->getLine() . PHP_EOL;
}

set_exception_handler('myExceptionHandler');

try {
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    throw $exception;
} catch (OutOfBoundsException $exception) {
    throw $exception;
} finally {
    if (isset($val)) {
        var_dump($val);
    } else {
        echo '异常将通过全局异常处理器处理...' . PHP_EOL;
    }
}

我们首先需要定义一个自定义的 myExceptionHandler 函数作为全局异常处理器,在这个函数中,我们需要传入异常对象作为参数,然后输出该异常类名、消息、出现异常的文件和行号,最后通过 set_exception_handler 函数将其注册为全局异常处理器。

在后续调用 getItemFromBook 时,由于捕获的异常抛给了上一层,但目前没有上一层调用代码,也就变成了未处理异常,最终这些异常会通过全局异常处理器进行兜底处理,执行上述代码,输出如下:

-w795

这里是将异常信息输出到了标准输出(STDOUT),如果是在线上生产环境,和自定义的全局错误处理器一样,你也可以将这些信息记录到日志文件中,或者发送到第三方日志处理服务。

自定义异常类

上面所有的异常都是 PHP 内置的异常类,除此之外,我们也可以根据需要创建自定义的异常类,只需要继承自 Exception 基类或者其子类即可,比如我们为索引不存在定义一个独立的异常类,并且继承自 LogicException 父类:

<?php
class IndexNotExistsException extends LogicException
{
    
}

暂时不需要编写任何方法,它可以继承祖先类 Exception 的所有 protected/public 方法和属性:

-w789

需要注意的是,Exception 类中的很多方法定义前面都有一个 final 关键字,通过该关键字修饰的方法不能被子类重写,如果我们试图这么做会报错:

-w722

另外,final 还可以用于修饰类,通过 final 修饰的类将不能被子类继承。

定义好自定义类之后,就可以在代码中捕获和处理了:

function getItemFromBook($book, $key)
{
    ...
    if (!key_exists($key, $book)) {
        throw new IndexNotExistsException("对应索引不存在!");
    }
    ...
}

...

try {
    $val = getItemFromBook($book, 'desc');
} catch (InvalidArgumentException $exception) {
    throw $exception;
} catch (IndexNotExistsException $exception) {
    throw $exception;
} finally {
    if (isset($val)) {
        var_dump($val);
    } else {
        echo '异常将通过全局异常处理器处理...' . PHP_EOL;
    }
}

执行上述代码,输出结果如下:

-w776

说明自定义异常类已经可以正常使用。

在实际项目开发中,可以结合自定义异常类和上述异常处理方式构建自己的异常处理体系。

小结

关于 PHP 面向对象编程我们就简单介绍到这里,通过前面的介绍,相信你已经对类和对象的实例化,类级别的静态方法,类功能的垂直扩展(继承、抽象类、接口)和水平扩展(对象组合、Trait)有了充分的认识,此外,PHP 类还支持特有的魔术方法,合理使用这些魔术方法可以进行一些很方便的初始化/善后清理工作,最后,对于程序中出现的错误和异常,可以通过一系列内置的机制进行捕获和处理。

下篇教程,我们将开始介绍 PHP 中如何连接 MySQL 数据库并进行增删改查操作。

上一篇: PHP 错误和异常处理(上)

下一篇: MySQL 入门、安装和客户端管理工具