道听途说
肚子大不可怕,可怕的是肚子里没有好东西。
——加菲猫
自PHP 5发布以来,异常(Exception)已作为面向对象的编程语言功能添加到PHP。根据定义,异常是程序执行期间的异常事件。在PHP中,Exception只是一个对象(Exception类的实例)。当发生异常时,PHP将暂停当前的执行流程并寻找一个处理程序,然后它将根据处理程序的代码继续执行。如果未找到任何处理程序,则将发出PHP致命错误,并显示“未捕获的异常...”消息,程序将终止。
1、什么时候使用异常
异常对于处理程序的异常情况很有用,但是,并不是所有错误情况的解决方案。有时,返回布尔值FALSE很好。有时,抛出异常比返回奇怪的错误代码要好得多。因此,了解何时使用Exception以及何时不使用Exception至关重要。
到现在为止,我们都知道在发生异常情况时应该抛出异常。但是,如果异常情况看起来相当武断,那么什么才算是“例外”情况呢?
这是一个很好的经验法则:由于特殊情况不会经常发生,因此,如果您向函数提供正确的值并删除抛出的异常,如果函数失败了,则错误地使用了该异常。
让我们看一些具体的例子:
1.1、Exception的一个很好的例子
这里有一个返回错误代码以指示错误情况的示例:
class User {
...
public function login()
{
if ($this->invalidUsernameOrPassword()) {
return -2;
}
if ($this->tooManyLoginAttempts()) {
return -1;
}
return 1;
}
public function redirectToUserPage()
{
...
}
}
客户端代码可能类似于以下内容:
$user = new User();
$result = $user->login();
if (-2 == $result) {
log('invalid username or password');
} else if (-1 == $result) {
log('too many login attempts');
} else if (1 == $result) {
$user->redirectToUserPage();
}
在这里,我们可以发现一些错误代码问题:
让我们用异常来重构代码:
class User {
...
public function login()
{
if ($this->invalidUsernameOrPassword()) {
throw new InvalidCredentialException('Invalid username or password');
}
if ($this->tooManyLoginAttempts()) {
throw new LoginAttemptsException('Too many login attempts');
}
}
public function redirectToUserPage()
{
...
}
}
客户端代码可以重构为:
try {
$user = new User();
$user->login();
$user->redirectToUserPage();
} catch (InvalidCredentialException $e) {
log($e->getMessage());
} catch (LoginAttemptsException $e) {
log($e->getMessage());
}
如我们所见,通过使用异常,第二个代码示例更加清楚地传达了有关错误的消息。除此之外,在客户端代码中,通过消除条件语句,代码变得不言自明。
1.2、滥用异常的情况
滥用的一种常见方式是使用异常来控制应用程序逻辑流。这不仅令人困惑,而且会减慢您的代码速度。再次强调,异常用于引发特殊情况。
以下是不鼓励滥用的异常的一个示例。
function register($email, $role) {
try {
if ($role == 'member') {
throw new CreateMemberException();
} else if ($role == 'admin') {
throw new CreateAdminException();
}
} catch (CreateMemberException $e) {
// 创建会员账户的相关代码
} catch (CreateAdminException $e) {
// 创建管理员账户的相关代码
}
}
函数register()使用异常来委派创建帐户任务。这显然违反了异常使用规则。尽管PHP并没有阻止你,但是你应该虔诚地禁止自己这样做。
2、如何使用异常
有四个关键字与使用Exception相关联。他们是:throw ,try ,catch ,finally 。 当异常事件发生时,将在方法中抛出异常(throw)对象。调用该方法的客户端通常会将方法放在try块中,并使用一些处理代码来捕获(catch)它。finaly块中的代码将确保能始终执行该块内的代码。
2.1、Throw
PHP中的所有异常都是Exception的类或子类。它在其构造函数中带有三个可选参数。
public __construct ([ string $message = "" [, int $code = 0 [, Exception $previous = NULL)
以下是抛出异常的PHP语法示例:
throw new Exception('一些错误信息');
这里的关键字是throw。注意,我们首先需要创建(new)一个异常对象。
2.2、Catch
当我们需要捕获异常时,我们将需要异常处理的代码放置在try-catch语块中,如下所示:
try {
methodThatThrowsExceptions();
} catch (Exception $e) {
//处理异常的相关代码
}
catch语块是我们放置处理程序代码的地方。详细的异常处理实现取决于应用程序设计。例如,我们可以尝试尽可能多地恢复异常,如果不可能,则可以将用户重定向到客户支持页面。如果我们不使用它,PHP最终将终止该程序,并向用户显示无意义的错误消息页面,通常我们不建议这样做。
2.3、异常冒泡效应
如果你使用过某种框架,则即使你从未为异常创建任何处理程序,也可能会处理异常。那是因为异常冒泡,你的框架最终将处理它们。异常冒泡效应的一个简单示例是:
function methodA()
{
throw new Exception('error from methodA');
}
function methodB()
{
methodA();
}
function methodC()
{
try {
methodB();
} catch (Exception $e) {
// 异常处理的相关代码
}
}
在示例代码中,当调用methodC时,它会调用methodB,后者将直接调用methodA。由于methodB不处理该异常,因此在methodA中引发了异常。然后,它会冒泡到达methodC,后者可以妥善处理异常。在此示例中,尽管methodC不会直接调用methodA,但由于异常会堆积到堆栈上,因此它仍会在末尾妥善处理。
2.4、多个catch语块
多个捕获块
一个方法可能包含不同的例外:一些可能自己直接抛出,有些可能从其底层堆栈冒泡。catch语块旨在处理多个异常,因此我们可以有多个catch语块来处理不同的异常。需要注意的是,捕获异常的职责很重要。
在多个catch语块中,PHP选择与引发的异常的类型匹配的第一个语块。定位捕获块的一个好的规则是从更具体的块到不太具体的块。
让我们看一个例子:
class ExceptionA extends Exception{}
class ExceptionB extends ExceptionA{}
try {
methodThatThrowsExceptionA();
} catch (ExceptionA $e) {
} catch (ExceptionB $e) {
} catch (Exception $e) {
}
在示例代码中,很明显异常发生将会选择ExceptionA的catch语块。现在,让我们更改方法来引发ExceptionB。
class ExceptionA extends Exception{}
class ExceptionB extends ExceptionA{}
try {
methodThatThrowsExceptionB();
} catch (ExceptionA $e) {
} catch (ExceptionB $e) {
} catch (Exception $e) {
}
你认为异常发生会选择哪个catch语块?答案仍然是ExceptionA。因为ExceptionA是ExceptionB的父类,所以当抛出ExceptionB时,ExceptionA catch块排在最前面,并且与抛出的异常的类型匹配,从而使ExceptionB是ExceptionA的实例。
可以通过将它们从较具体的类型定位到较不具体的类型来解决此问题,如下所示:
class ExceptionA extends Exception{}
class ExceptionB extends ExceptionA{}
try {
methodThatThrowsExceptionB();
} catch (ExceptionB $e) {
} catch (ExceptionA $e) {
} catch (Exception $e) {
}
2.5追踪消息
由于可以在程序的任何位置抛出异常,因此找到根本原因非常重要。Exception提供了各种API,可以轻松地跟踪异常的来源。
从PHP手册文件中提取了七个公共方法:
我们可以使用它们来跟踪引发的异常的详细信息:
以下我们来演示一下:
出于演示的目的,我们假设我们有一个createAccount()方法,当电子邮件地址无效时,该方法将引发Exception。
function createAccount($email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('In valid email address');
}
return sprintf('account creation email is sent to %s', $email);
}
如果触发异常,我们可以获得什么信息?
function linSeparator()
{
return PHP_EOL.'----------------------------------'.PHP_EOL;
}
try {
createAccount('test');
} catch (Exception $e) {
echo $e->getMessage();
echo linSeparator();
echo $e->getPrevious();
echo linSeparator();
echo $e->getCode();
echo linSeparator();
echo $e->getFile();
echo linSeparator();
print_r($e->getTrace());
echo linSeparator();
echo $e->getTraceAsString();
}
从CLI运行脚本,我们得到以下信息:
In valid email address
----------------------------------
----------------------------------
0
----------------------------------
/Users/xu/Desktop/Exception/trace.php
----------------------------------
Array
(
[0] => Array (
[file] => /Users/xu/Desktop/Exception/trace.php
[line] => 19
[function] => createAccount
[args] => Array
(
[0] => test
)
)
)
----------------------------------
#0 /Users/xu/Desktop/Exception/trace.php(19): createAccount('test')
除了明显的跟踪信息外,我们还可以说,实例化异常对象时,默认代码为0,并且先前的异常为null。
public __construct (string $message = "" , int $code = 0 , Exception $previous = NULL);
我们还要在这里解决的另一点是,在实例化异常时(而不是在引发异常时)会创建一个异常。因此,异常API将为您提供有关实例化异常时间的信息。
例如,在下面的方法中,Exception :: getLine将返回2。
function methodThrowException()
{
$exception = new Exception('error from methodThrowException'); // line 2
throw $exception; // line 3
}
2.6、finally
在PHP 5.5之前,PHP是没有finally语块。问题浮出水面。如果我们想确保无论选择哪个catch语块,程序最终都能运行一段代码,则必须将这段代码放入每个catch语块中。
为了解决这个问题,从PHP 5.5开始引入了finally语块。finally语块中的代码将最终在catch语块之后执行。我们甚至可以只使用try / catch而不使用catch。
finally语块是我们进行清理工作的地方。诸如回滚数据库事务,关闭数据库连接,释放文件锁等任务。它的用法非常简单。
例如,将其与try / catch块一起使用:
try {
createAccount('test');
} catch (Exception $e) {
echo $e->getMessage();
} finally {
echo 'Close Database Connection';
}
仅使用try / finally例子:
try {
createAccount('test');
} finally {
echo 'Close Database Connection';
}
3、创建自定义异常
引发自定义异常允许客户端代码以公认的方式处理错误情况。例如,当引发数据库异常时,可以合理地完全地关闭进城。但是,在用户输入无效的情况下,我们可能只想记录一条错误消息。
通过创建自定义异常,我们可以主动表达代码的错误情况。这不仅可以帮助客户端避坑,还可以为他们提供足够的信息来自信地处理错误情况。
由于PHP 5.x中的所有异常均以Exception作为基础,因此我们实际上是在扩展Exception来创建自定义异常。在以下示例中,让我们重新查看我们以前的代码。
class User {
...
public function login()
{
if ($this->invalidUsernameOrPassword()) {
throw new InvalidCredentialException('不合法的用户名或密码');
}
if ($this->tooManyLoginAttempts()) {
throw new LoginAttemptsException('太多次登录尝试');
}
}
public function redirectToUserPage()
{
...
}
}
这里有两个自定义异常(InvalidCredentialException和LoginAttemptsException)。它们实际上应该属于一种类型。他们将被分配不同的消息。
由于InvalidCredentialException和LoginAttemptsException是无效登录运行时错误的错误情况,因此可以创建一个名为InvalidLoginException的异常并将其用于上述两种错误情况是合理的。
创建仅一行代码的自定义异常非常简单。
class InvalidLoginException extends Exception {}
我们可以重构先前的代码以使用新创建的异常类:
class User {
...
public function login()
{
if ($this->invalidUsernameOrPassword()) {
throw new InvalidLoginException('Invalid username or password');
}
if ($this->tooManyLoginAttempts()) {
throw new InvalidLoginException('Too many login attempts');
}
}
public function redirectToUserPage()
{
...
}
}
3.1、一点技巧
如果我们将InvalidLoginException与太多不同的消息一起使用,则可能会很快出现潜在的问题。这个问题很容易说明。
想象一下在代码中的某个地方,当用户帐户被阻止时,我们需要引发另一个InvalidLoginException。我们将抛出确切的InvalidLoginException,但带有不同的消息。同样的事情再次发生,我们将重复同样的动作。不同的消息归纳加起来。现在想象一下针对不同类型的异常执行此操作。作为开发人员,我们会迷路。
因此,这里有一个小技巧:将异常创建任务转移到InvalidLoginException类。
class InvalidLoginException extends Exception
{
public static function invalidUsernameOrPassword() {
return new static('Invalid username or password');
}
public static function tooManyLoginAttempts() {
return new static('Too many login attempts');
}
}
现在,客户端的代码改为:
class User {
...
public function login()
{
if ($this->invalidUsernameOrPassword()) {
throw InvalidLoginException::invalidUsernameOrPassword();
}
if ($this->tooManyLoginAttempts()) {
throw InvalidLoginException::tooManyLoginAttempts();
}
}
public function redirectToUserPage()
{
...
}
}
与之前的if块中的一行相比,当异常的实例转移到功能块时,我们将获得更多的空间和自由来做更多有趣的事情。
通过将所有代码都放在异常类本身所在的集中位置,不仅创建了更易于维护的代码库,而且还使客户有机会快速浏览他们期望的确切异常。
4、SPL 异常
创建你自己的自定义例外是很棒的,但是要想为自定义异常取一个有意义的名字,确实需要花费一些精力。命名很困难,可以说是编程中最困难的事情之一。
标准PHP库(SPL)提供了一组标准异常。为了自己的目的,我们应该使用它们。它们是一个涵盖了常见错误情况的列表,如果我们自己解决问题,则可以节省我们的精力。此外,我们还可以扩展这些标准Exception,使它们更适合于我们自己的领域。
在本节中,我们将介绍14个SPL异常,以最简单的方式进行解释,以便你下次可以在自己的项目中使用它们。
-LogicException (extends Exception)
--BadFunctionCallException
--BadMethodCallException
--DomainException
--InvalidArgumentException
--LengthException
--OutOfRangeException
-RuntimeException (extends Exception)
--OutOfBoundsException
--OverflowException
--RangeException
--UnderflowException
--UnexpectedValueException
SPL异常有两个主要类别。它们是(逻辑异常)LogicException和(运行时异常)RuntimeException,在它们各自的下面,还有几个子异常类描述了更具体的错误情况。
4.1、LogicExcetpion
不难看出LogicException涵盖了与逻辑相关的错误情况。由于它是一些其他特定异常的父类,因此有点通用。当您的代码返回或接收非逻辑内容时,就会出现逻辑错误。当确定错误情况是逻辑错误时,如果无法从其子类中找到更好的匹配项,请使用LogicException。
当不存在的函数被调用或向函数提供错误的参数时,将抛出此异常。由于此异常涵盖函数范围,而不是类中的方法,因此它通常由PHP抛出。
当某个类的不存在的方法被调用,或者为该方法提供了错误的参数时,会抛出BadFunctionCallException。尽管此异常类似于BadFunctionCallException,但它是为类范围设计的。
域在这里指的是我们的代码适用的业务。当参数按其数据类型有效但对域无效时,可以引发DomainException。
例如,在通用图像处理函数transformImage($ imageType)中,当$ imageType包含无效的图像类型时,应引发DomainException。对于此域,无效的图像类型是域错误。
顾名思义,这很简单:提供无效参数时应将其抛出。
PHP5引入了类型提示,但是它还不适用于标量类型,例如int,string。为了使其工作,当标量类型不符合要求时,我们将抛出InvalidArgumentException。
当某些东西的长度无效时,我们可以使用此异常。例如,密码必须至少为8个字符。
访问无效索引时,请使用此异常。此处的关键字是range(范围)。
5. 运行时异常RuntimeException
RuntimeException是从诸如Java之类的编译语言派生的名称。在Java中,异常主要有两种:检查异常和运行时异常。直到处理完所有检查的异常(在catch块中),编译器才会编译代码。运行时异常只能在运行时检测,并且不需要将这些异常放置在catch块中。
由于PHP不是编译语言,因此我们可以将其“编译时间”视为编写代码的时间,并将其“运行时间”视为代码执行的时间。可以在开发时检测到“编译时”异常,例如无效的数据类型参数。
为避免混淆,请记住,上面讨论的逻辑异常是针对“编译时”的。
RuntimeException的子类包含更多特定的方案。如果无法从其子类中找到更好的匹配项,请使用此异常。
调用无效索引时使用此异常。不要与OutOfRangeException混淆,OutOfBoundsException是运行时异常。
例如,当用户创建数组数据结构并且调用无效索引时,应引发OutOfBoundsException。而尝试使用8来获取星期几应该抛出OutOfRangeException。
$booksList = array();
$bookList[5]; // OutOfBoundsException
$dayOfWeek = $calendar->day(8); // OutOfRangeException
当要求容量有限的容器填充超出其容纳能力的容器时,会引发此异常。
此异常适用于与“运行时”范围相关的一般性错误情况。
与OverflowException相反的是UnderflowException。当要求一个空容器删除元素时,可以引发此异常。
顾名思义,当引发或访问意外值时,我们将引发此异常。
以上就是PHP SPL提供的所有异常。对于错误情况,我们应该始终抛出最准确的异常。不可避免地,一个异常可能适合多个异常,在这种情况下,可以选择一个异常。
有意义的异常消息对可维护项目大有帮助。