Yii源码阅读笔记 - 错误/异常处理

2015-09-14 一

By youngsterxyf

概述

PHP区分“错误”(Error)和“异常”(Exception)。“错误”通常是由PHP内部函数抛出,表示运行时问题,当然也可以通过函数trigger_erroruser_error抛出一个用户级别的error/warning/notice信息。但在引入面向对象之后,相比使用trigger_error抛出错误,使用throw抛出异常更常用。

对于“错误”,PHP允许配置报告哪些级别/类型错误、是否(向用户)展示错误、是否对错误记录日志、错误日志记到哪,分别对应php.ini中的配置项:error_reportingdisplay_errorslog_errorserror_log。详细信息见这里

对于应用程序内层调用抛出的“异常”,一般可以在外层中使用try...catch来捕获并自定义处理过程。但对于“错误”(PHP运行时抛出或者应用程序使用trigger_error抛出的)或者对于-无法使用try...catch来捕获可能的异常/为了做到即使忘记捕获的异常也能得到自定义处理-的情况,该怎么办?对此,PHP提供了函数set_error_handlerset_exception_handler来注册错误/异常自定义处理过程。如果在程序的执行流中先后多次调用了set_error_handlerset_exception_handler,后一次注册的处理过程会覆盖前一次的,但可以通过函数restore_error_handlerrestore_exception_handler来恢复前一次注册的异常处理过程。

之所以写这篇文章,是因为最近在工作中犯了一个低级错误:应用程序中有个API对于不合法的请求参数直接抛出异常(throw new Exception("xxx")),却忘了try...catch捕捉,导致异常被Yii框架(我们的应用基于Yii开发)通过set_exception_handler注册的方法处理 - 响应500,之后这个API被一个扫描器拼命扫,导致出现很多500响应,触发了告警。

分析

我们来看看Yii框架在哪个地方注册错误/异常处理过程?处理过程是什么样的?

Yii框架在请求处理初始化过程中,在CApplication类(见文件base/CApplication.php)的构造方法中调用了:

<?php
$this->initSystemHandlers();

initSystemHandlers的实现如下:

<?php
/**
 * Initializes the class autoloader and error handlers.
 */
protected function initSystemHandlers() { // 注:如果不想使用Yii框架注册的handleException,可以在初始化应用实例之前,定义常量YII_ENABLE_EXCEPTION_HANDLER值为false if(YII_ENABLE_EXCEPTION_HANDLER) set_exception_handler(array($this,'handleException')); // 注:YII_ENABLE_ERROR_HANDLER也是如此 if(YII_ENABLE_ERROR_HANDLER) set_error_handler(array($this,'handleError'),error_reporting()); } 

其中注册的方法handleExceptionhandleError实现分别如下:

<?php
public function handleException($exception) { // disable error capturing to avoid recursive errors // 这句注释是啥意思? restore_error_handler(); restore_exception_handler(); // 生成并记录日志信息 $category='exception.'.get_class($exception); if($exception instanceof CHttpException) $category.='.'.$exception->statusCode; // php <5.2 doesn't support string conversion auto-magically $message=$exception->__toString(); if(isset($_SERVER['REQUEST_URI'])) $message.="\nREQUEST_URI=".$_SERVER['REQUEST_URI']; if(isset($_SERVER['HTTP_REFERER'])) $message.="\nHTTP_REFERER=".$_SERVER['HTTP_REFERER']; $message.="\n---"; Yii::log($message,CLogger::LEVEL_ERROR,$category); try { // 将异常封装成事件,并触发事件,从而触发监听该事件的处理过程 $event=new CExceptionEvent($this,$exception); $this->onException($event); // 如果事件并没有被处理(即没有监听该事件的处理过程)或者所有处理过程都没有将事件的handled属性置为true,则还得自己处理一下 if(!$event->handled) { // try an error handler if(($handler=$this->getErrorHandler())!==null) $handler->handle($event); else $this->displayException($exception); } } catch(Exception $e) { $this->displayException($e); } try { // 尝试触发onEndRequest事件 $this->end(1); } catch(Exception $e) { // use the most primitive way to log error $msg = get_class($e).': '.$e->getMessage().' ('.$e->getFile().':'.$e->getLine().")\n"; $msg .= $e->getTraceAsString()."\n"; $msg .= "Previous exception:\n"; $msg .= get_class($exception).': '.$exception->getMessage().' ('.$exception->getFile().':'.$exception->getLine().")\n"; $msg .= $exception->getTraceAsString()."\n"; $msg .= '$_SERVER='.var_export($_SERVER,true); error_log($msg); exit(1); } } 
<?php
public function handleError($code,$message,$file,$line) { if($code & error_reporting()) { // disable error capturing to avoid recursive errors restore_error_handler(); restore_exception_handler(); // 生成并记录日志信息 $log="$message ($file:$line)\nStack trace:\n"; // debug_backtrace() 产生一条 PHP 的回溯跟踪 $trace=debug_backtrace(); // skip the first 3 stacks as they do not tell the error position if(count($trace)>3) $trace=array_slice($trace,3); foreach($trace as $i=>$t) { if(!isset($t['file'])) $t['file']='unknown'; if(!isset($t['line'])) $t['line']=0; if(!isset($t['function'])) $t['function']='unknown'; $log.="#$i {$t['file']}({$t['line']}): "; if(isset($t['object']) && is_object($t['object'])) $log.=get_class($t['object']).'->'; $log.="{$t['function']}()\n"; } if(isset($_SERVER['REQUEST_URI'])) $log.='REQUEST_URI='.$_SERVER['REQUEST_URI']; Yii::log($log,CLogger::LEVEL_ERROR,'php'); try { // 将错误封装成事件,并触发 Yii::import('CErrorEvent',true); $event=new CErrorEvent($this,$code,$message,$file,$line); $this->onError($event); // 如果错误事件未被处理 if(!$event->handled) { // try an error handler if(($handler=$this->getErrorHandler())!==null) $handler->handle($event); else $this->displayError($code,$message,$file,$line); } } catch(Exception $e) { $this->displayException($e); } try { // 尝试触发onEndRequest事件 $this->end(1); } catch(Exception $e) { // use the most primitive way to log error $msg = get_class($e).': '.$e->getMessage().' ('.$e->getFile().':'.$e->getLine().")\n"; $msg .= $e->getTraceAsString()."\n"; $msg .= "Previous error:\n"; $msg .= $log."\n"; $msg .= '$_SERVER='.var_export($_SERVER,true); error_log($msg); exit(1); } } } 

从上面代码可以看到,方法handleException的关键部分(handleError类似)为:

<?php
// 将异常封装成事件,并触发事件,从而触发监听该事件的处理过程
$event=new CExceptionEvent($this,$exception); $this->onException($event); // 如果事件并没有被处理(即没有监听该事件的处理过程)或者所有处理过程都没有将事件的handled属性置为true,则还得自己处理一下 if(!$event->handled) { // try an error handler if(($handler=$this->getErrorHandler())!==null) $handler->handle($event); else $this->displayException($exception); } 

其中方法onException的实现如下:

<?php
public function onException($event) { $this->raiseEvent('onException',$event); } 

raiseEvent方法实现如下:

<?php
public function raiseEvent($name,$event) { // 根据事件名称,如onException,找到注册到该事件的处理过程,逐个触发调用。 // 所有该事件注册的处理过程存放在$this->_e[$name]中 $name=strtolower($name); if(isset($this->_e[$name])) { foreach($this->_e[$name] as $handler) { if(is_string($handler)) call_user_func($handler,$event); elseif(is_callable($handler,true)) { if(is_array($handler)) { // an array: 0 - object, 1 - method name list($object,$method)=$handler; if(is_string($object)) // static method call call_user_func($handler,$event); elseif(method_exists($object,$method)) $object->$method($event); else throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".', array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>$handler[1]))); } else // PHP 5.3: anonymous function call_user_func($handler,$event); } else throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".', array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>gettype($handler)))); // stop further handling if param.handled is set true if(($event instanceof CEvent) && $event->handled) return; } } elseif(YII_DEBUG && !$this->hasEvent($name)) throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.', array('{class}'=>get_class($this), '{event}'=>$name))); } 

那么是如何注册事件的处理过程的呢?

在类CComponent(见文件base/CComponent.phpCApplication类间接继承自该类)中定义了一对方法:attachEventHandler(将处理过程绑定到某事件)和detachEventHandler(将处理过程从事件解绑)。

方法attachEventHandler的实现如下:

<?php
public function attachEventHandler($name,$handler) { $this->getEventHandlers($name)->add($handler); } 

其中getEventHandlers实现如下:

<?php
public function getEventHandlers($name) { // 可以关注一下方法hasEvent // 检查是否存在$name对应的事件 if($this->hasEvent($name)) { $name=strtolower($name); if(!isset($this->_e[$name])) $this->_e[$name]=new CList; // 返回对应事件的处理过程列表 return $this->_e[$name]; } else throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.', array('{class}'=>get_class($this), '{event}'=>$name))); } 

回到“方法handleException的关键部分”,在事件的handled属性没有置为true的情况下,会调用方法getErrorHandler取到内置的一个处理过程,该方法实现如下:

<?php
public function getErrorHandler()
{ // 获取名为errorHandler的组件,该组件默认会在CApplication类的registerCoreComponents方法中注册, // 见http://blog.xiayf.cn/2014/11/13/read-yii-code-3/一文的说明 return $this->getComponent('errorHandler'); } 

名为errorHandler的组件默认为类CErrorHandler(见文件base/CErrorHandler.php),当然也可以配置覆盖默认行为。

CErrorHandler类的handle方法实现如下:

<?php
public function handle($event) { // set event as handled to prevent it from being handled by other event handlers $event->handled=true; if($this->discardOutput) { $gzHandler=false; foreach(ob_list_handlers() as $h) { if(strpos($h,'gzhandler')!==false) $gzHandler=true; } // the following manual level counting is to deal with zlib.output_compression set to On // for an output buffer created by zlib.output_compression set to On ob_end_clean will fail for($level=ob_get_level();$level>0;--$level) { if(!@ob_end_clean()) ob_clean(); } // reset headers in case there was an ob_start("ob_gzhandler") before if($gzHandler && !headers_sent() && ob_list_handlers()===array()) { if(function_exists('header_remove')) // php >= 5.3 { header_remove('Vary'); header_remove('Content-Encoding'); } else { header('Vary:'); header('Content-Encoding:'); } } } // 异常和错误都可以调用handle方法 if($event instanceof CExceptionEvent) $this->handleException($event->exception); else // CErrorEvent $this->handleError($event); } 

其中方法handleExceptionhandleError实现分别如下:

<?php
protected function handleException($exception) { $app=Yii::app(); // 如果是Web应用 if($app instanceof CWebApplication) { if(($trace=$this->getExactTrace($exception))===null) { $fileName=$exception->getFile(); $errorLine=$exception->getLine(); } else { $fileName=$trace['file']; $errorLine=$trace['line']; } $trace = $exception->getTrace(); foreach($trace as $i=>$t) { if(!isset($t['file'])) $trace[$i]['file']='unknown'; if(!isset($t['line'])) $trace[$i]['line']=0; if(!isset($t['function'])) $trace[$i]['function']='unknown'; unset($trace[$i]['object']); } $this->_error=$data=array( // 如果抛出的异常是CHttpException类型,使用该异常自身的statusCode作为HTTP响应码,否则HTTP响应码为500 // 所以在有意让Yii框架来处理抛出的异常时,需要明确指定异常的类型! 'code'=>($exception instanceof CHttpException)?$exception->statusCode:500, 'type'=>get_class($exception), 'errorCode'=>$exception->getCode(), 'message'=>$exception->getMessage(), 'file'=>$fileName, 'line'=>$errorLine, 'trace'=>$exception->getTraceAsString(), 'traces'=>$trace, ); if(!headers_sent()) header("HTTP/1.0 {$data['code']} ".$this->getHttpHeader($data['code'], get_class($exception))); // 判断异常类型 // 对于CHttpException,也按照error来处理 if($exception instanceof CHttpException || !YII_DEBUG) $this->render('error',$data); else { if($this->isAjaxRequest()) $app->displayException($exception); else $this->render('exception',$data); } } // 如果是终端应用(console application),则直接展示异常 else $app->displayException($exception); } 
<?php
protected function handleError($event) { $trace=debug_backtrace(); // skip the first 3 stacks as they do not tell the error position if(count($trace)>3) $trace=array_slice($trace,3); $traceString=''; foreach($trace as $i=>$t) { if(!isset($t['file'])) $trace[$i]['file']='unknown'; if(!isset($t['line'])) $trace[$i]['line']=0; if(!isset($t['function'])) $trace[$i]['function']='unknown'; $traceString.="#$i {$trace[$i]['file']}({$trace[$i]['line']}): "; if(isset($t['object']) && is_object($t['object'])) $traceString.=get_class($t['object']).'->'; $traceString.="{$trace[$i]['function']}()\n"; unset($trace[$i]['object']); } $app=Yii::app(); // 如果是Web应用 if($app instanceof CWebApplication) { // 判断错误类型 switch($event->code) { case E_WARNING: $type = 'PHP warning'; break; case E_NOTICE: $type = 'PHP notice'; break; case E_USER_ERROR: $type = 'User error'; break; case E_USER_WARNING: $type = 'User warning'; break; case E_USER_NOTICE: $type = 'User notice'; break; case E_RECOVERABLE_ERROR: $type = 'Recoverable error'; break; default: $type = 'PHP error'; } // HTTP响应码为500 $this->_error=$data=array( 'code'=>500, 'type'=>$type, 'message'=>$event->message, 'file'=>$event->file, 'line'=>$event->line, 'trace'=>$traceString, 'traces'=>$trace, ); if(!headers_sent()) header("HTTP/1.0 500 Internal Server Error"); if($this->isAjaxRequest()) $app->displayError($event->code,$event->message,$event->file,$event->line); elseif(YII_DEBUG) // 开了debug,则作为exception来处理 $this->render('exception',$data); else $this->render('error',$data); } else $app->displayError($event->code,$event->message,$event->file,$event->line); } 

上面的代码最终显示异常/错误信息,是通过方法render、以及应用实例的displayErrordisplayException方法来完成。

render

<?php
protected function render($view,$data) { // 注意这个地方,如果配置了errorAction,则可以指定目标controller的某个action来处理错误 /*  * 配置方式:  * 'components' => array(  * 'errorHandler' => array(  * 'errorAction'=>'api/index/error',  * ),  * ...  */ if($view==='error' && $this->errorAction!==null) Yii::app()->runController($this->errorAction); else { // additional information to be passed to view $data['version']=$this->getVersionInfo(); $data['time']=time(); $data['admin']=$this->adminInfo; // 看看下面getViewFile的实现 include($this->getViewFile($view,$data['code'])); } } protected function getViewFile($view,$code) { $viewPaths=array( Yii::app()->getTheme()===null ? null : Yii::app()->getTheme()->getSystemViewPath(), Yii::app() instanceof CWebApplication ? Yii::app()->getSystemViewPath() : null, YII_PATH.DIRECTORY_SEPARATOR.'views', ); foreach($viewPaths as $i=>$viewPath) { if($viewPath!==null) { // 看看下面getViewFileInternal的实现 $viewFile=$this->getViewFileInternal($viewPath,$view,$code,$i===2?'en_us':null); if(is_file($viewFile)) return $viewFile; } } } protected function getViewFileInternal($viewPath,$view,$code,$srcLanguage=null) { $app=Yii::app(); if($view==='error') { $viewFile=$app->findLocalizedFile($viewPath.DIRECTORY_SEPARATOR."error{$code}.php",$srcLanguage); if(!is_file($viewFile)) $viewFile=$app->findLocalizedFile($viewPath.DIRECTORY_SEPARATOR.'error.php',$srcLanguage); } else $viewFile=$viewPath.DIRECTORY_SEPARATOR."exception.php"; return $viewFile; } 

上面代码的逻辑是 - 对于error类型的信息,Yii会依次在以下目录中寻找名为error{$code}.php文件来展示错误/异常信息:

  1. WebRoot/themes/ThemeName/views/system
  2. WebRoot/protected/views/system
  3. yii/framework/views

如果没有找到,则以相同的次序在这些目录中查找error.php文件。

对于exception类型信息,则是查找exception.php文件。

所以如果应用开发过程需要定制4xx、5xx的错误页面,可以在WebRoot/protected/views/systemWebRoot/themes/ThemeName/views/system放置对应的错误模板页面。

displayError

<?php
public function displayError($code,$message,$file,$line) { if(YII_DEBUG) { echo "<h1>PHP Error [$code]</h1>\n"; echo "<p>$message ($file:$line)</p>\n"; echo '<pre>'; $trace=debug_backtrace(); // skip the first 3 stacks as they do not tell the error position if(count($trace)>3) $trace=array_slice($trace,3); foreach($trace as $i=>$t) { if(!isset($t['file'])) $t['file']='unknown'; if(!isset($t['line'])) $t['line']=0; if(!isset($t['function'])) $t['function']='unknown'; echo "#$i {$t['file']}({$t['line']}): "; if(isset($t['object']) && is_object($t['object'])) echo get_class($t['object']).'->'; echo "{$t['function']}()\n"; } echo '</pre>'; } else { echo "<h1>PHP Error [$code]</h1>\n"; echo "<p>$message</p>\n"; } } 

displayException

<?php
public function displayException($exception) { if(YII_DEBUG) { echo '<h1>'.get_class($exception)."</h1>\n"; echo '<p>'.$exception->getMessage().' ('.$exception->getFile().':'.$exception->getLine().')</p>'; echo '<pre>'.$exception->getTraceAsString().'</pre>'; } else { echo '<h1>'.get_class($exception)."</h1>\n"; echo '<p>'.$exception->getMessage().'</p>'; } } 

总结

由上述分析可知,基于Yii框架开发应用时,有以下几点注意事项:

  • 可以通过配置组件errorHandlererrorAction属性来定制异常/错误处理过程
  • 可以通过在WebRoot/themes/ThemeName/views/systemWebRoot/protected/views/system放置模板(名为error{$code}.php)来定制错误/异常展示方式
  • 在有意抛出异常由Yii框架捕获时,需明确异常的类型是否应为CHttpException,只有CHttpException实例初始化时指定的code才能成为HTTP响应码
  • 可以对事件onErroronException绑定事件处理过程,进行额外的处理,比如记录错误/异常、触发告警等

参考资料

转载于:https://www.cnblogs.com/sunscheung/p/4864190.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值