PHP各种异常和错误的拦截方法及发生致命错误时进行报警
在日常开发中,大多数人的做法是在开发环境时开启调试模式,在产品环境关闭调试模式。在开发的时候可以查看各种错误、异常,但是在线上就把错误显示的关闭。
上面的情形看似很科学,有人解释为这样很安全,别人看不到错误,以免泄露重要信息...
但是你有没有遇到这种情况,线下好好的,一上线却运行不起来也找不到原因...
一个脚本,跑了好长一段时间,一直没有问题,有一天突然中断了,然后了也没有任何记录都不造啥原因...
线上一个付款,别人明明付了款,但是我们却没有记录到,自己亲自去实验,却是好的...
种种以上,都是因为大家关闭了错误信息,并且未将错误、异常记录到日志,导致那些随机发生的错误很难追踪。这样矛盾就来了,即不要显示错误,又要追踪错误,这如何实现了?
以上问题都可以通过PHP的错误、异常机制及其内建函数'set_exception_handler','set_error_handler','register_shutdown_function'来实现
'set_exception_handler'函数用于拦截各种未捕获的异常,然后将这些交给用户自定义的方式进行处理
'set_error_handler'函数可以拦截各种错误,然后交给用户自定义的方式进行处理
'register_shutdown_function'函数是在PHP脚本结束时调用的函数,配合'error_get_last'可以获取最后的致命性错误
这个思路大体就是把错误、异常、致命性错误拦截下来,交给我们自定义的方法进行处理,我们辨别这些错误、异常是否致命,如果是则记录的数据库或者文件系统,然后使用脚本不停的扫描这些日志,发现严重错误立即发送邮件或发送短信进行报警
首先我们定义错误拦截类,该类用于将错误、异常拦截下来,用我们自己定义的处理方式进行处理,该类放在文件名为'errorHandler.class.php'中,代码如下
/** *文件名称:baseErrorHandler.class.php *摘要:错误拦截器父类 */ require'errorHandlerException.class.php';//异常类 classerrorHandler { public$argvs=array(); public$memoryReserveSize=262144;//备用内存大小 private$_memoryReserve;//备用内存 /** *方法:注册自定义错误、异常拦截器 *参数:void *返回:void */ publicfunctionregister() { ini_set('display_errors',0); set_exception_handler(array($this,'handleException'));//截获未捕获的异常 set_error_handler(array($this,'handleError'));//截获各种错误此处切不可掉换位置 //留下备用内存供后面拦截致命错误使用 $this->memoryReserveSize>0&&$this->_memoryReserve=str_repeat('x',$this->memoryReserveSize); register_shutdown_function(array($this,'handleFatalError'));//截获致命性错误 } /** *方法:取消自定义错误、异常拦截器 *参数:void *返回:void */ publicfunctionunregister() { restore_error_handler(); restore_exception_handler(); } /** *方法:处理截获的未捕获的异常 *参数:Exception$exception *返回:void */ publicfunctionhandleException($exception) { $this->unregister(); try { $this->logException($exception); exit(1); } catch(Exception$e) { exit(1); } } /** *方法:处理截获的错误 *参数:int$code错误代码 *参数:string$message错误信息 *参数:string$file错误文件 *参数:int$line错误的行数 *返回:boolean */ publicfunctionhandleError($code,$message,$file,$line) { //该处思想是将错误变成异常抛出统一交给异常处理函数进行处理 if((error_reporting()&$code)&&!in_array($code,array(E_NOTICE,E_WARNING,E_USER_NOTICE,E_USER_WARNING,E_DEPRECATED))) {//此处只记录严重的错误对于各种WARNINGNOTICE不作处理 $exception=newerrorHandlerException($message,$code,$code,$file,$line); $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); array_shift($trace);//trace的第一个元素为当前对象移除 foreach($traceas$frame) { if($frame['function']=='__toString') {//如果错误出现在__toString方法中不抛出任何异常 $this->handleException($exception); exit(1); } } throw$exception; } returnfalse; } /** *方法:截获致命性错误 *参数:void *返回:void */ publicfunctionhandleFatalError() { unset($this->_memoryReserve);//释放内存供下面处理程序使用 $error=error_get_last();//最后一条错误信息 if(errorHandlerException::isFatalError($error)) {//如果是致命错误进行处理 $exception=newerrorHandlerException($error['message'],$error['type'],$error['type'],$error['file'],$error['line']); $this->logException($exception); exit(1); } } /** *方法:获取服务器IP *参数:void *返回:string */ finalpublicfunctiongetServerIp() { $serverIp=''; if(isset($_SERVER['SERVER_ADDR'])) { $serverIp=$_SERVER['SERVER_ADDR']; } elseif(isset($_SERVER['LOCAL_ADDR'])) { $serverIp=$_SERVER['LOCAL_ADDR']; } elseif(isset($_SERVER['HOSTNAME'])) { $serverIp=gethostbyname($_SERVER['HOSTNAME']); } else { $serverIp=getenv('SERVER_ADDR'); } return$serverIp; } /** *方法:获取当前URI信息 *参数:void *返回:string$url */ publicfunctiongetCurrentUri() { $uri=''; if($_SERVER["REMOTE_ADDR"]) {//浏览器浏览模式 $uri='http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; } else {//命令行模式 $params=$this->argvs; $uri=$params[0]; array_shift($params); for($i=0,$len=count($params);$i<$len;$i++) { $uri.=''.$params[$i]; } } return$uri; } /** *方法:记录异常信息 *参数:errorHandlerException$e错误异常 *返回:boolean是否保存成功 */ finalpublicfunctionlogException($e) { $error=array( 'add_time'=>time(), 'title'=>errorHandlerException::getName($e->getCode()),//这里获取用户友好型名称 'message'=>array(), 'server_ip'=>$this->getServerIp(), 'code'=>errorHandlerException::getLocalCode($e->getCode()),//这里为各种错误定义一个编号以便查找 'file'=>$e->getFile(), 'line'=>$e->getLine(), 'url'=>$this->getCurrentUri(), ); do { //$e->getFile().':'.$e->getLine().''.$e->getMessage().'('.$e->getCode().')' $message=(string)$e; $error['message'][]=$message; }while($e=$e->getPrevious()); $error['message']=implode("\r\n",$error['message']); $this->logError($error); } /** *方法:记录异常信息 *参数:array$error=array( *'time'=>int, *'title'=>'string', *'message'=>'string', *'code'=>int, *'server_ip'=>'string' *'file'=>'string', *'line'=>int, *'url'=>'string', *); *返回:boolean是否保存成功 */ publicfunctionlogError($error) { /*这里去实现如何将错误信息记录到日志*/ } }
上述代码中,有个'errorHandlerException'类,该类放在文件'errorHandlerException.class.php'中,该类用于将错误转换为异常,以便记录错误发生的文件、行号、错误代码、错误信息等信息,同时其方法'isFatalError'用于辨别该错误是否是致命性错误。这里我们为了方便管理,将错误进行编号并命名。该类的代码如下
/** *文件名称:errorHandlerException.class.php *摘要:自定义错误异常类该类继承至PHP内置的错误异常类 */ classerrorHandlerExceptionextendsErrorException { publicstatic$localCode=array( E_COMPILE_ERROR=>4001, E_COMPILE_WARNING=>4002, E_CORE_ERROR=>4003, E_CORE_WARNING=>4004, E_DEPRECATED=>4005, E_ERROR=>4006, E_NOTICE=>4007, E_PARSE=>4008, E_RECOVERABLE_ERROR=>4009, E_STRICT=>4010, E_USER_DEPRECATED=>4011, E_USER_ERROR=>4012, E_USER_NOTICE=>4013, E_USER_WARNING=>4014, E_WARNING=>4015, 4016=>4016, ); publicstatic$localName=array( E_COMPILE_ERROR=>'PHPCompileError', E_COMPILE_WARNING=>'PHPCompileWarning', E_CORE_ERROR=>'PHPCoreError', E_CORE_WARNING=>'PHPCoreWarning', E_DEPRECATED=>'PHPDeprecatedWarning', E_ERROR=>'PHPFatalError', E_NOTICE=>'PHPNotice', E_PARSE=>'PHPParseError', E_RECOVERABLE_ERROR=>'PHPRecoverableError', E_STRICT=>'PHPStrictWarning', E_USER_DEPRECATED=>'PHPUserDeprecatedWarning', E_USER_ERROR=>'PHPUserError', E_USER_NOTICE=>'PHPUserNotice', E_USER_WARNING=>'PHPUserWarning', E_WARNING=>'PHPWarning', 4016=>'Customer`sError', ); /** *方法:构造函数 *摘要:相关知识请查看http://php.net/manual/en/errorexception.construct.php * *参数:string$message异常信息(可选) *int$code异常代码(可选) *int$severity *string$filename异常文件(可选) *int$line异常的行数(可选) *Exception$previous上一个异常(可选) * *返回:void */ publicfunction__construct($message='',$code=0,$severity=1,$filename=__FILE__,$line=__LINE__,Exception$previous=null) { parent::__construct($message,$code,$severity,$filename,$line,$previous); } /** *方法:是否是致命性错误 *参数:array$error *返回:boolean */ publicstaticfunctionisFatalError($error) { $fatalErrors=array( E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING ); returnisset($error['type'])&&in_array($error['type'],$fatalErrors); } /** *方法:根据原始的错误代码得到本地的错误代码 *参数:int$code *返回:int$localCode */ publicstaticfunctiongetLocalCode($code) { returnisset(self::$localCode[$code])?self::$localCode[$code]:self::$localCode[4016]; } /** *方法:根据原始的错误代码获取用户友好型名称 *参数:int *返回:string$name */ publicstaticfunctiongetName($code) { returnisset(self::$localName[$code])?self::$localName[$code]:self::$localName[4016]; }
在错误拦截类中,需要用户自己定义实现错误记录的方法('logException'),这个地方需要注意,有些错误可能在一段时间内不断发生,因此只需记录一次即可,你可以使用错误代码、文件、行号、错误详情生成一个MD5值用于记录该错误是否已经被记录,如果在规定时间内(一个小时)已经被记录过则不需要再进行记录
然后我们定义一个文件,用于实例化以上类,捕获各种错误、异常,该文件命名为'registerErrorHandler.php',内如如下
/* *使用方法介绍: *在入口处引入该文件即可,然后可以在该文件中定义调试模式常量'DEBUG_ERROR' * *<?php * *require'registerErrorHandler.php'; * *?> */ /** *调试错误模式: *0=>非调试模式,不显示异常、错误信息但记录异常、错误信息 *1=>调试模式,显示异常、错误信息但不记录异常、错误信息 */ define('DEBUG_ERROR',0); require'errorHandler.class.php'; classregisterErrorHandler { /** *方法:注册异常、错误拦截 *参数:void *返回:void */ publicstaticfunctionregister() { global$argv; if(DEBUG_ERROR) {//如果开启调试模式 ini_set('display_errors',1); return; } //如果不开启调试模式 ini_set('error_reporting',-1); ini_set('display_errors',0); $handler=newerrorHandler(); $handler->argvs=$argv;//此处主要兼容命令行模式下获取参数 $handler->register(); } } registerErrorHandler::register();
剩下的就是需要你在你的入口文件引入该文件,定义调试模式,然后实现你自己记录错误的方法即可
需要注意的是,有些错误在你进行注册之前已经发生并且导致脚本中断是无法记录下来的,因为此时'registerErrorHandler::register()'尚未执行已经中断了
还有就是'set_error_handler'这个函数不能捕获下面类型的错误E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARNING,这个可以在官方文档中看到,但是本处无妨,因为以上错误是解析、编译错误,这些都没有通过,你是不可能发布上线的