错误处理
无论您的应用程序设计得多么简单和直观,都会有用户使用不当或出现随机连接错误的情况,您的代码必须做好处理这些情况的准备,以便为用户提供尽可能好的体验。我们将这些情况称为异常:一种识别与我们预期不同的情况的语言元素。
try…catch 块
只要您认为有必要,您的代码就可以手动抛出异常。例如,以 Unique
trait 中的 setId
方法为例。多亏了类型提示,我们强制 ID 必须是数字,但也仅此而已。如果有人试图设置一个负数 ID,会发生什么情况呢?现在的代码允许这样做,但根据您的偏好,您希望避免这样做。这将是出现异常的好地方。让我们看看如何添加这种检查和相应的异常:
public function setId($id) {
if ($id < 0) {
throw new \Exception('Id cannot be negative.');
}
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
}
正如你所看到的,异常是类 exception
的对象。请记住在类名后加上反斜杠,除非你想在文件顶端包含使用 Exception;
。Exception
类的构造函数需要一些可选参数,其中第一个参数是异常信息。异常类的实例本身没有任何作用,它们必须被抛出才能被程序注意到。
让我们试着强制程序抛出这个异常。为此,让我们尝试创建一个 ID 为负数的客户。在 init.php
文件中添加以下内容:
$basic = new Basic(-1, "name", "surname", "email");
如果您现在在浏览器中进行尝试,PHP 会抛出一个致命的错误,说出现了一个未捕获的异常,这是预期的行为。对于 PHP 来说,异常是无法恢复的,因此它会停止执行。这并不理想,因为您希望只向用户显示一条错误信息,然后让他们再试一次。
您可以而且应该使用 try…catch
块捕获异常。在 try
代码块中插入可能产生异常的代码,如果出现异常,PHP 就会跳转到 catch
代码块。让我们看看它是如何工作的:
public function setId(int $id) {
try {
if ($id < 0) {
throw new Exception('Id cannot be negative.');
}
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
} catch (Exception $e) {
echo $e->getMessage();
}
}
如果我们在浏览器中测试最后一段代码,就会看到 catch
代码块中打印的消息。在异常实例上调用 getMessage
方法,就会得到异常信息—创建对象时的第一个参数。但请记住,构造函数的参数是可选的;因此,如果不确定异常是如何产生的,就不要过于依赖异常消息,因为它可能是空的。
请注意,抛出异常后,try
代码块中的其他代码都不会被执行;PHP 会直接进入 catch
代码块。此外,该代码块会得到一个参数,即抛出的异常。在这里,类型提示是强制性的—你很快就会知道为什么。将参数命名为 $e
是一种广泛使用的惯例,尽管使用描述性不强的变量名并不是一种好的做法。
到目前为止,在这个示例中使用异常并没有任何真正的好处。一个简单的 if…else
代码块就能完成同样的工作,不是吗?但异常的真正威力在于它可以跨方法传播。也就是说,setId
方法抛出的异常如果没有被捕获,就会传播到调用该方法的任何地方,从而让我们在那里捕获它。这一点非常有用,因为代码中的不同地方可能希望以不同的方式处理异常。要了解如何做到这一点,让我们移除 setId
中插入的 try…catch
,并在 init.php
文件中插入以下代码:
try {
$basic = new Basic(-1, "name", "surname", "email");
} catch (Exception $e) {
echo 'Something happened when creating the basic customer: ' . $e->getMessage();
}
前面的示例说明了捕获传播异常的作用:我们可以更具体地了解发生了什么,因为我们知道异常抛出时用户试图做什么。在本例中,我们知道用户正试图创建客户,但该异常可能是在试图更新现有客户的 ID 时抛出的,因此需要不同的错误信息。
finally 块
在处理异常时,还可以使用第三个代码块:finally
代码块。该代码块添加在 try…catch
代码块之后,是可选的。事实上,catch
块也是可选的;限制条件是 try
之后必须有至少一个 finally
块。因此,你可能会遇到以下三种情况:
// scenario 1: the whole try-catch-finally
try {
// code that might throw an exception
} catch (Exception $e) {
// code that deals with the exception
} finally {
// finally block
}
// scenario 2: try-finally without catch
try {
// code that might throw an exception
} finally {
// finally block
}
// scenario 3: try-catch without finally
try {
// code that might throw an exception
} catch (Exception $e) {
// code that deals with the exception
}
当 try
块或 catch
块全部执行完毕后,finally
块中的代码才会被执行。因此,如果没有异常,在 try
代码块中的所有代码执行完毕后,PHP 将执行 finally
代码块中的代码。另一方面,如果 try
代码块中出现异常,PHP 就会跳转到 catch
代码块,在执行完所有代码后,也会执行 finally
代码块。
为了测试此功能,让我们实现一个包含 try…catch…finally
块的函数,尝试用给定的 ID(通过参数)创建一个客户,并记录发生的所有操作。您可以将以下代码段添加到 init.php
文件中:
function createBasicCustomer($id)
{
try {
echo "\nTrying to create a new customer.\n";
return new Basic($id, "name", "surname", "email");
} catch (Exception $e) {
echo "Something happened when creating the basic customer: " . $e->getMessage() . "\n";
} finally {
echo "End of function.\n";
}
}
createBasicCustomer(1);
createBasicCustomer(-1);
如果您尝试这样做,您的浏览器将显示以下输出 - 请记住显示页面的源代码以查看其格式是否正确:

结果可能与你的预期不同。第一次调用函数时,我们能够顺利创建对象,这意味着我们执行了返回语句。在一个正常的函数中,这应该是它的结束,但由于我们是在 try…catch…finally
块中,所以我们仍然需要执行 finally
代码!第二个示例看起来更直观,从 try
跳到 catch
,然后再跳到 finally
块。
在处理昂贵的资源(如数据库连接)时,finally
块非常有用。在第 5 章 "使用数据库" 中,您将看到如何使用它们。根据连接的类型,使用后必须关闭连接,以便其他用户可以连接。无论函数是否抛出异常,finally
块都用于关闭这些连接。
捕获不同类型的异常
异常已经被证明是有用的,但还有一个重要功能需要展示:捕获不同类型的异常。正如你已经知道的,异常是类 Exception
的实例,与其他任何类一样,它们可以被扩展。从该类扩展的主要目的是创建不同类型的异常,但我们不会在内部添加任何逻辑—当然,你也可以这样做。让我们创建一个从 Exception
扩展而来的类,用于识别与无效 ID 相关的异常。将这段代码放在 src/Exceptions/InvalidIdException.php
文件中:
<?php
namespace Bookstore\Exceptions;
use Exception;
class InvalidIdException extends Exception {
public function __construct($message = null) {
$message = $message ?: 'Invalid id provided.';
parent::__construct($message);
}
}
InvalidIdException
类从 Exception
类扩展而来,因此可以作为一个类被抛出。该类的构造函数包含一个可选参数 $message
。下面两行代码很有意思:
-
?:
运算符是条件运算符的简写版本,其工作原理如下:如果左边的表达式不为假,则返回左边的表达式,否则返回右边的表达式。在这里,我们希望使用用户提供的信息,或者在用户没有提供信息的情况下使用默认信息。有关更多信息和用法,请访问 PHP 文档 http://php.net/manual/en/language.operators.comparison.php 。 -
parent::__construct
将调用父类的构造函数,即异常类的构造函数。正如你已经知道的,该构造函数的第一个参数是异常信息。你可以说,既然我们是从 Exception 类扩展而来,就不需要调用任何函数,因为我们可以直接编辑该类的属性。避免这样做的原因是让父类管理自己的属性。设想一下,由于某种原因,在未来的 PHP 版本中,Exception
更改了消息的属性名称。如果直接修改,就必须在代码中修改,但如果使用构造函数,就不用担心。内部实现比外部接口更容易改变。
我们可以使用这个新的异常来代替通用的异常。将其替换为您的 Unique trait,如下所示:
throw new InvalidIdException('Id cannot be a negative number.');
您可以看到,我们仍在发送一条信息:这是因为我们希望更加具体。但是,如果不发送消息,异常也能正常工作。再试一次你的代码,你会发现没有任何变化。
现在设想一下,我们的数据库非常小,不能允许超过 50 个用户。我们可以创建一个新的异常来识别这种情况,比方说,src/ Exceptions/ExceededMaxAllowedException.php
:
<?php
namespace Bookstore\Exceptions;
use Exception;
class ExceededMaxAllowedException extends Exception {
public function __construct($message = null) {
$message = $message ?: 'Exceeded max allowed.';
parent::__construct($message);
}
}
让我们修改我们的 trait,以检查这种情况。在设置 ID 时,如果该 ID 大于 50,我们就可以认为用户数量已达到上限:
public function setId(int $id) {
if ($id < 0) {
throw new InvalidIdException('Id cannot be a negative number.');
}
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
if ($this->id > 50) {
throw new ExceededMaxAllowedException('Max number of users is 50.');
}
}
现在,前面的函数会抛出两种不同的异常: InvalidIdException
和 ExceededMaxAllowedException
。在捕获它们时,您可能希望根据捕获的异常类型采取不同的行为方式。还记得必须在 catch
代码块中声明参数吗?您可以根据需要添加多个捕获块,并在每个捕获块中指定不同的异常类。代码可以如下所示
function createBasicCustomer(int $id)
{
try {
echo "\nTrying to create a new customer with id $id.\n";
return new Basic($id, "name", "surname", "email");
} catch (InvalidIdException $e) {
echo "You cannot provide a negative id.\n";
} catch (ExceededMaxAllowedException $e) {
echo "No more customers are allowed.\n";
} catch (Exception $e) {
echo "Unknown exception: " . $e->getMessage();
}
}
createBasicCustomer(1);
createBasicCustomer(-1);
createBasicCustomer(55);
如果您尝试此代码,您应该会看到以下输出:

请注意,我们在这里捕获了三个异常:两个新异常和一个通用异常。这样做的原因是,可能会有其他代码抛出与我们定义的异常类型不同的异常,我们需要用通用异常类定义一个捕获块来捕获它,因为所有异常都将从通用异常类扩展而来。当然,这完全是可选的,如果不这样做,异常就会直接传播。
请注意 catch 块的顺序。PHP 会按照您定义的顺序使用捕获块。因此,如果您的第一个 catch 是针对异常的,那么其余的块将永远不会被执行,因为所有异常都是从该类扩展而来的。用下面的代码试试看:
try {
echo "\nTrying to create a new customer with id $id.\n";
return new Basic($id, "name", "surname", "email");
} catch (Exception $e) {
echo 'Unknown exception: ' . $e->getMessage() . "\n";
} catch (InvalidIdException $e) {
echo "You cannot provide a negative id.\n";
} catch (ExceededMaxAllowedException $e) {
echo "No more customers are allowed.\n";
}
您从浏览器获得的结果将始终来自第一次捕获:
