如何组织PHP中的异常

   2016-11-10 0
核心提示:本文的主题是怎么组织php的异常?在大型项目中异常往往被我们忽略,但是如果前期没有很好的规划好,越到项目后期,重构的成本会越大。 在实际工作中,对于错误的处理,我们一帮都是直接返回错误号,然后从最内层一层一层往外面传,最后将错误返回给用户,很少

如何组织PHP中的异常

本文的主题是怎么组织php的异常?在大型项目中异常往往被我们忽略,但是如果前期没有很好的规划好,越到项目后期,重构的成本会越大。

在实际工作中,对于错误的处理,我们一帮都是直接返回错误号,然后从最内层一层一层往外面传,最后将错误返回给用户,很少使用异常,可能是因为公司里最初写代码比较早,13,14年开始使用php,当时第一批使用者是从C转过来的,从而没有使用异常,导致现在都16年了,php都出7了,我们在实际代码中还是没有使用异常,我前不久在项目中引入了异常,但也是简单的使用 try catch ,没有很多的经验,网上搜索也只是简单的一些使用例子,没有说在大型项目中怎么去使用,最近也是在读The Clean Architecture in php,深知代码组织的重要性,如果前期没有很好的组织好,后期的维护,重构代价都会很大,今天看到两篇文章:

Structuring PHP Exceptions

A Crash Course of Changes to Exception Handling in PHP 7

所以就有了本文。写这篇文章的目的是探讨一些在实际中怎么使用异常的方式,也希望得到大家的反馈,大家平时在开发中是怎么使用异常的?如何组织的。

为什么还使用异常?

在讨论使用异常之前,我们得统一认识:使用异常对项目是有益的。我们看看没有异常的时候,我们的处理方式。

返回错误号

functionfoo($arrInput){
if($arrInput['user_id']<0){
return-1;// 参数错误
 }
// something else
}

当程序遇到错误时返回一个错误码,使用这种方式的好处是:我们每次在调用完函数后,都会检查返回值,当出现错误的时候,马上进行处理。

但是坏处也很明显:错误的处理和正常的业务逻辑耦合在了一起,我们平时开发中一个很恼人的感触就是:写一个业务逻辑,可能异常错误处理就占了2/3的代码,愁人啊,于是有人就发明了 异常

在php中对错误的处理有两种,一种是error和warnings,另一种是异常。

errors & warnings

php中的errors和warnings来源于过程式的代码,在过程式代码中,我们按照既定的步骤一步一步执行,此时如果出现了错误,我们必须要将程序的控制权接管过来,在PHP中是通过 set_error_handler 方法来设置处理函数的,但是这种方式没能提供一种有效的错误恢复手段,你可能除了打印下错误信息后,没有足够的错误发生时的上下文信息让你来恢复错误了。

exceptions

一般我们使用异常的代码如下:

try{
 find_slash(string);
} catch(AnException& e) {
//Handle exception
}

这样做的好处是:程序逻辑和错误处理分离了。你可以看到函数是如何工作的,同时也可以看到失败时候是怎么处理的。另外,现在可以提供更多的异常发生的上下信息,帮助你从发生的异常中恢复出来。

举个例子:当从数据库中获取一条记录的时候发生了异常,我们可以根据异常的不同类型,采取不同的结果。如果异常时由于没有我们想要的id记录,我们可能返回一个 NullObject 是更好的方式,但如果异常是由于数据库连接的断开,我们可能会继续抛出异常,让异常被更上层的函数看到,因为这个异常在此处我们已经没有能够恢复的方法了。

通过SPL来构建异常

Standard PHP Library (SPL) 标准库中提供了一些 predefined set of exceptions ,我们可以基于这些预定于的异常进行扩展,得到满足我们自己需求的代码。这样子做的好处是,我们能够很方便的捕获(catch)这些异常。

此处提供一个组织异常的方案: standard set of exception groupings 是一些预定义的异常,每次在使用的使用,通过 composer 引入。通过引入这一抽象层的目的是:让我能更好的区分想要捕获异常的粒度。

standard set of exception groupings 中的每个异常,都extend了SPL中的异常,而且实现了 BrightNucleus\Exception\ExceptionInterface 接口,这么做可以方便我只捕获框架相关的异常,通过只捕获实现了接口的异常。

下面列举了捕获不同粒度的异常的方法:

  1. Catch all exceptions
    catch( Exception $exception ) {}
  2. Catch all exceptions thrown by a Bright Nucleus library
    catch( BrightNucleus\Exception\ExceptionInterface $exception ) {}
  3. Catch a specific SPL exception (BrightNucleus or not)
    catch( LogicException $exception )
  4. Catch a specific SPL exception thrown by a Bright Nucleus library
    catch( BrightNucleus\Exception\LogicException $exception ) {}

命名规范

目前命名的一个原则是:

  1. 该异常如果代表一个具体的错误,则使用一个过去时态的语句表名错误发生的原因
  2. 如果异常是一个基类,需要别的类进行扩展,则统一后缀 Exception

看一个具体的例子:

假设我们有一个功能是从文件中读取内容,可能会有3种错误发生:

  1. 文件名不合法
  2. 文件不存在
  3. 文件不可读

此时会有3种错误:

FileNameWasNotValid extends InvalidArgumentException
FileWasNotFound extends InvalidArgumentException
FileWasNotReadable extends RuntimeException

此时具体的错误都是过去式的句子,而基类都是带有统一后缀的。

通过构造函数捕获异常逻辑

我们一般在实例化异常的时候,都是直接在使用的时候才去new出来,但是这种方式导致异常的代码可能会比正常的业务逻辑还负责,非常不适合阅读,而且将相同的实例化逻辑放的到处都是,也符合代码重用的原则,我们举个例子:

publicfunctionrender( $view ){

if( !$this->views->has( $view ) ) {
 $message = sprintf(
"The View "%s" does not exist.",
 json_encode( $view )
 );

thrownewViewWasNotFound( $message );
 }

echo$this->views->get( $view )->render();
}

上面的代码中异常的处理逻辑比正常的业务逻辑还多,我们重构下,将异常的构建封装起来:

classViewWasNotFoundextendsInvalidArgumentException{

publicstaticfunctionfromView( $view, $code = null, Exception $previous = null ){
 $message = sprintf(
"The View "%s" does not exist.",
 json_encode( $view )
 );

returnnewstatic( $message, $code, $previous );
 }
}

我们可能会有多个构造函数,每个构造函数有不同的应用场景,此时我们再来写我们的 render 函数:

publicfunctionrender( $view ){

if( !$this->views->has( $view ) ) {
throwViewWasNotFound::fromView( $view );
 }

echo$this->views->get( $view )->render();
}

现在代码就非常简洁了。

异常捕获

问:我们需要捕获什么异常?

答:只捕获当前上下文下能够处理的异常。

如果当前操作返回 NullObject 也ok,那在最外层套一个 catch( Exception $exception ) {} 就完全ok。但是如果当前操作只有正确才能保证后续操作继续,那你可能就需要捕获那些你当前能恢复的异常,那些不能恢复的异常,则让它往更上层去。

在SPL中,我们定义了两大类异常:

  1. Logic exceptions

    逻辑异常是那些由于开发者的错误而导致的异常。你可能在请求一些不存在的值,或者调用传递的参数不对等等。这些异常在开发中都需要我们马上处理掉的。在理想情况下,这些逻辑异常在实际生产系统中是不应该出现的。

  2. runtime exception

    运行时异常是一些在开发中不能控制的异常,如:数据库链接的异常断开,文件的读写权限不对等等。这些错误是无法避免的,我们不可能开发一个没有错误的系统,我们能做得只是当这些错误发生的时候,尽快的去通知系统管理员,而不是代码出现 fatal

这就是为什么我们在开发中需要在某一软件层捕获运行时错误,而对于逻辑错误,我们尽可能让它在开发时就让他们暴露出来,好让我们在开发时就解决它。

中心化的Error处理函数

我们将逻辑异常都pass through,没有去捕获,那么作为一个web应用,我们不能让用户无响应啊,因此我们需要通过一个中心化的处理函数来捕获所有我们没有处理的异常。

捕获后,我们一般的工作是:记录这些异常,记录调用栈,方便我们去分析解决这些问题。

对于这个工作,我推荐使用 BooBoo 来做。

总结

此处总结下我们的原则:

  • 对于运行时异常,我们尽量捕获然后进行处理,重要的上报错误,让管理员知道系统异常,而对于逻辑异常我们则是将其尽可能详细的记录下来,因为这些错误理论上是不应该出现在生产环境中。
  • 我们在捕获异常的时候,只捕获在该层级能处理的异常,对于不能处理的则让它到上一层上去。
  • 我们需要一个全局的异常处理函数,处理如返回html,json这种格式问题,以及处理错误信息的转换(隐藏系统内部错误信息),错误的记录,现场环境的保存等公共逻辑。

一个示例

讲了这么多,还是那句话

talk is cheap, show me the code

我们基于的一个基本代码是:

$user = $this->usersGateway->fetchOneById($userId);

if(!$user) {
thrownewException('User with the ID: '. $userId .' does not exist');
}

用户定义异常

上面针对找不到user的情况,我们只是简单的抛出了异常。但是上面的问题是:仅仅抛出异常不足以帮助我们定位问题,单一的异常类型,不能让我们针对不同的类型做出不同行为,因此解决方法是自定义异常。

classUserNotFoundExceptionextendsRuntimeException
{
}

//...

thrownewUserNotFoundException('User with the ID: '. $userId .' does not exist');

格式化异常

现在我们已经有了异常类,并且异常的生成和异常消息都是异常类本身的职责,因此我们根据单一职责(SRP)将其组织到异常类中:

classUserNotFoundExceptionextendsRuntimeException
{
publicstaticfunctionforUserId(string $userId):self
 {
returnnewself(sprintf(
'User with the ID: %s does not exist',
 $userId
 ));
 }
}

在使用异常的地方我们简单的调用下面的代码:

throwUserNotFoundException::forUserId($userId);

聚合异常

根据单一职责(SRP)我们将相同异常放到一起,不同的功能拆分出来,看例子:

classUserExceptionextendsException
{
publicstaticfunctionforEmptyEmail():self
 {
returnnewself("User's email must not be empty");
 }

publicstaticfunctionforInvalidEmail(string $email):self
 {
returnnewself(sprintf(
'%s is not a valid email address',
 $email
 ));
 }

publicstaticfunctionforNonexistentUser(string $userId):self
 {
returnnewself(sprintf(
'User with the ID: %s does not exist',
 $userId
 ));
 }
}

在上面的例子中,异常类 UserException 有两个功能,第一个负责User的验证异常,另一个则是没有用户的异常,因此我们应该拆分为两个:

classInvalidUserExceptionextendsDomainException
{
publicstaticfunctionforEmptyEmail():self
 {
returnnewself("User's email address must not be empty");
 }

publicstaticfunctionforInvalidEmail(string $email):self
 {
returnnewself(sprintf(
'%s is not a valid email address',
 $email
 ));
 }
}
classUserNotFoundExceptionextendsRuntimeException
{
publicstaticfunctionforUserId(string $userId):self
 {
returnnewself(sprintf(
'User with the ID: %s does not exist',
 $userId
 ));
 }
}

此时我们就能针对不同的异常类采取不同的措施,可能我们会根据异常类返回合适的 HTTP status codes。

异常代码

异常的构造函数接受code作为第二个参数,所以我们可以通过不同的错误码来标志不同的错误。

classUserNotFoundExceptionextendsRuntimeException
{
publicstaticfunctionforUserId(string $userId):self
 {
returnnewself(
 sprintf(
'User with the ID: %s does not exist',
 $userId
 ), 
 ErrorCodes::ERROR_USER_NOT_FOUND
 );
 }
}

我们会将所有的错误码都放到一个文件中,方便管理。

组件级别的异常

当我们提供一个库给别人使用的时候,我们可能希望能够捕获我们库级别的异常,这通过一个模式 Marker Interface 可以实现:

namespaceApp\Domain\Exception;

interfaceExceptionInterface
{
}

classUserNotFoundExceptionextendsRuntimeExceptionimpementsExceptionInterface
{
publicstaticfunctionforUserId(string $userId):self
 {
returnnewself(
 sprintf(
'User with the ID: %s does not exist',
 $userId
 ), 
 ErrorCodes::ERROR_USER_NOT_FOUND
 );
 }
}

我们通过在每个命名空间都声明一个 ExceptionInterface 类来实现,这样我们就可以通过代码 try{}catch(ExceptionInterface $e){} 来捕获所有本库的错误。

错误处理

上代码:

classUserControllerextendsBaseController
{
publicfunctionviewUserAction(RequestInterface $request)
 {
try{
 $user = $this->userService->get($request->get('id'));

returnnewJsonResponse($user->toArray());
 } catch(\Exception$ex) {
returnnewJsonResponse([
'error'=> $ex->getCode(),
'message'=> $ex->getMessage(),
 ], 500);
 }
 }
}

上面的处理中,我们在controller中通过一个最外层的 try{}catch{} 捕获了所有异常,但是我们针对不同的需求可能会有不同的返回格式的要求,可能我们需要针对参数的不同返回html或者json格式,另外我们也不希望底层的错误信息,如:数据库连接失败,这样子的错误信息直接返回给调用方,那怎么解决呢?

这就要用到PHP的全局错误处理函数了,通过 set_error_handler 来设置,另外推荐除了 BooBoo 另外一个开源库: Whoops ,能很好的解决这个问题。

你的观点

相信你在实际工作中肯定也遇到过好多类似的困扰,你在实际工作中也有你自己的一套解决方案,期待你的分享,让更多的人知道好的优秀的方案,所以期待你在评论区写下你的方案。

这是 php异常系列 的第一篇,你的鼓励是我继续写下去的动力,期待我们共同进步。

 
标签: PHP
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • php-fpm进程管理的三种模式 phpfpm子进程
    php-fpm进程管理的三种模式 phpfpm子进程
    php-fpm解读-进程管理的三种模式—程序媛大丽标明转载以示尊重 感谢原作者的分享。php-fpm进程管理一共有三种模式:ondemand、static、dynamic,我们可以在同一个fpm的master配置三种模式,看下图1。php-fpm的工作模式和nginx类似,都是一个master,多个worke
    03-08
  • nginx和php-fpm 是使用 tcp socket 还是 unix s
    tcp socket允许通过网络进程之间的通信,也可以通过loopback进行本地进程之间通信。unix socket允许在本地运行的进程之间进行通信。分析从上面的图片可以看,unix socket减少了不必要的tcp开销,而tcp需要经过loopback,还要申请临时端口和tcp相关资源。但是
    03-08
  • [PHP8] 我参加了PHP8工程师认证初学者考试beta考试
    [PHP8] 我参加了PHP8工程师认证初学者考试beta
    前几天,2022/08/05,PHP工程师认证机构PHP8 技术员认证初级考试宣布实施考试将于 2023 年春季开始。和 beta 测试完成于 2022/09/11所以我收到了。一般社团法人BOSS-CON JAPAN(代表理事:Tadashi Yoshimasa,地点:东京都世田谷区,以下简称“BOSS-CON JAPAN
    03-08
  • 将 PHP Insights 放入旧版 PJ 不是很好吗?谈论
    将 PHP Insights 放入旧版 PJ 不是很好吗?谈论
    介绍在最近的PHP系统开发中,感觉故事在理所当然包含静态分析工具的前提下进行。我的周围现有代码很脏,我很久以前安装了工具,但几乎没有检查已经观察到许多这样的案例。 (这是小说。而不是像 0 或 100 这样不允许单行错误的静态分析,一点一点,逐渐我想介
    03-08
  • PHP基于elasticsearch全文搜索引擎的开发 php使
    1.概述:全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选。Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接
    02-09
  • php视图操作
    一、视图的基本介绍         视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。        使用视图需要MySQL5及以后的版本支持。        下面是视图的一些常见应用:        重用SQL语句;        简化复杂的S
    02-09
  • php中图像处理的常用函数 php图形图像处理技术
    php中图像处理的常用函数 php图形图像处理技术
    1.imagecreate()函数imagecreate()函数是基于一个调色板的画布。?php $im = imagecreate(200,80);                //创建一个宽200,高80的画布。$white = imagecolorallocate($im,225,35,180);     //设置画布的背景颜色imagegif($im);
    02-09
  • PHP安全之webshell和后门检测
    PHP安全之webshell和后门检测
    基于PHP的应用面临着各种各样的攻击:XSS:对PHP的Web应用而言,跨站脚本是一个易受攻击的点。攻击者可以利用它盗取用户信息。你可以配置Apache,或是写更安全的PHP代码(验证所有用户输入)来防范XSS攻击SQL注入:这是PHP应用中,数据库层的易受攻击点。防范
    02-09
  • php使用时间戳保存时间的意义 PHP获取时间戳
    时间戳记录的是格林尼治时间,使用date格式化的时候会根据你程序设置的不同时区显示不同的时间。如果使用具体时间,则还需要进行多一步转换。
    02-09
  • PHP 获取提交表单数据方法
    PHP $_GET 和 $_POST变量是用来获取表单中的信息的,比如用户输入的信息。PHP表单操作在我们处理HTML表单和PHP表单时,我们要记住的重要一点是:HTML页面中的任何一个表单元素都可以自动的用于PHP脚本:表单举例: htmlbodyform action="welcome.php" method
    02-09
点击排行