对PHP的错误和异常,很多人只知道在框架中怎么使用,框架封装这些东西的原理是怎么样的?设计时需要注意些什么?如何在自己写的PHP框架中整合异常和错误?看完下面的内容后相信你能迎刃而解了!
一、异常处理的目的
从一个简单的例子说起,有一个网站,需要登录和注册的功能,假设处理这两个功能的类为Auth,则这个类中应该有login()和register()的接口,分别提供登录和注册的服务。
<?php
class Auth{
// login interface
public function login(){
}
// register interface
public function register(){
}
}
登录和注册要接受参数吧!假设参数通过post方式传递,登录的参数有user(用户名)、passwd(密码)、code(验证码)。要完成登录功能,首先要对参数进行检验,如果参数不符合要求,比如为空,则不满足要求,向前台反馈登录失败及原因,很自然想到用if...else...结果解决实现。
public function login(){
$user = addslashes($_POST['user']);
$passwd = addlashes($_POST['passwd']);
$code = addlashes($_POST['code']);
// 参数检验
if(empty($user)){
return array('error' => '请填写你的用户名');
}
if(empty($passwd)){
return array('error' => '请填写你的密码');
}
if(empty($code)){
return array('error' => '请填写验证码');
}
// 调用数据检验接口
$mod = new Model();
$mod->login($user, $passwd, $code);
}
好像OK哦!在Model层的,login接口又要对验证码的正确性还有数据库是否存在这么一个用户进行检验,假如都没问题,返回前台登录成功的提示消息。假如失败,又要通过if...else结构返回类似参数检验的数组array(‘error’=> 错误原因)。
如果接口有更多的参数,处理的步骤更加繁琐,出错的情况更加复杂,程序代码中充斥的if...else会严重降低程序的可读性,并且让错误的定位和检查变得很困难。比如上传文件的接口,你要对文件类型、大小、上传超时时间、文件保存目录、文件路径插入数据库等进行检查核验,可能出现错误的情况更多,这时用if..else已成为一种负担。
很多时候我们有一些代码专门用来应对错误发生的情况。比如上传成功一个文件,如果文件目录记录进数据库失败,那么应该把这个已经成功上传的文件删除掉,否则它成功了也没有存在的意义,这个删除的代码就是错误的的专用代码。如果用if...else来写,会导致正常的逻辑代码和错误补救代码混杂在一起,可读性很差,而且想修改补救代码时也很不方便。这时我们特别希望有一种机制,把正常代码和补救代码隔离开来,这种机制就是异常处理机制。
如果我们使用了异常处理机制,上面的代码可以优化为如下
public function login(){
$user = addslashes($_POST['user']);
$passwd = addlashes($_POST['passwd']);
$code = addlashes($_POST['code']);
try{
if(empty($user)) throw new Exception('请填写你的用户名');
if(empty($user)) throw new Exception('请填写你的密码');
if(empty($user)) throw new Exception('请填写验证码');
// 调用数据检验接口
$mod = new Model();
$mod->login($user, $passwd, $code);
}catch (Exception $e){
$error = $e->getMessage();
return json_encode(array('error' => $error));
}
}
在Model层接口抛出的异常也可以被catch所接受,这里只是简单地向前台返回异常信息,如果想对异常进行处理,或者屏蔽一些敏感信息显示到前台,完全可以在catch里进行处理。
二、PHP中的异常机制
从上面的例子可以看出,PHP中的异常,指的是不符合程序正常逻辑或者说不符合预期的情况,而不是语法错误。
PHP不会自动抛出异常,所有的异常都是在程序员的的逻辑思考下考虑特定的情况产生的,也就是说,PHP中的异常都是手动抛出才能被捕获的。
那PHP中的异常和错误有什么区别呢?PHP中的错误属于自身问题,是语法或语言环境存在问题导致的、让编译器无法通过检查和正常运行的情况。比如实例化一个不存在的类,这属于错误,当程序发生错误时,意味着程序员必须细致地根据错误提示一个个检查消除。
那么如何正确使用PHP中的异常呢?
一般的使用形式如下:
try{
// 抛出异常或者在调用的接口方法中抛出异常
}catch(diy1Exception $e){
// 异常情况1处理代码
}catch(diy2Exception $e){
// 异常情况2处理代码
}catch(Exception $e){
// 缺省异常情况处理代码
}
这样处理逻辑清晰,异常处理代码跟正常代码实现隔离,而且修改调整方便。
三、PHP跟Java异常机制的对比
跟PHP相比,Java就显得非常干练统一了,区别对比总结如下:
1、Java报错的唯一途径就是抛出异常,没有错误和异常之分。Java提供了丰富的异常接口和相应的实现API,这使得异常的使用简单方便。
2、Java的异常既能系统API抛出,也能程序员根据业务逻辑需求抛出。只要发生了错误,肯定会抛出异常,这要求程序员必须正视出现的问题;而PHP可以设置错误报错的级别,可能在兼容性等方面有一定的好处,但是从总体上看语言的健壮性差很多。
四、PHP中的错误机制
跟异常相比,错误更有价值,因为错误是不可预料的。
(1)PHP中的错误级
PHP中的错误级别由16种,列表如下:
值 | 常量 | 说明 | 备注 |
---|---|---|---|
1 | E_ERROR (integer) | 致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。 | |
2 | E_WARNING (integer) | 运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。 | |
4 | E_PARSE (integer) | 编译时语法解析错误。解析错误仅仅由分析器产生。 | |
8 | E_NOTICE (integer) | 运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知。 | |
16 | E_CORE_ERROR (integer) | 在PHP初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由PHP引擎核心产生的。 | since PHP 4 |
32 | E_CORE_WARNING (integer) | PHP初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由PHP引擎核心产生的。 | since PHP 4 |
64 | E_COMPILE_ERROR (integer) | 致命编译时错误。类似E_ERROR, 但是是由Zend脚本引擎产生的。 | since PHP 4 |
128 | E_COMPILE_WARNING (integer) | 编译时警告 (非致命错误)。类似 E_WARNING,但是是由Zend脚本引擎产生的。 | since PHP 4 |
256 | E_USER_ERROR (integer) | 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生 | since PHP 4 |
512 | E_USER_WARNING (integer) | 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 | since PHP 4 |
1024 | E_USER_NOTICE (integer) | 用户产生的通知信息。类似 E_NOTICE, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 | since PHP 4 |
2048 | E_STRICT (integer) | 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。 | since PHP 5 |
4096 | E_RECOVERABLE_ERROR (integer) | 可被捕捉的致命错误。 它表示发生了一个可能非常危险的错误,但是还没有导致PHP引擎处于不稳定的状态。 如果该错误没有被用户自定义句柄捕获 (参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。 | since PHP 5.2.0 |
8192 | E_DEPRECATED (integer) | 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告。 | since PHP 5.3.0 |
16384 | E_USER_DEPRECATED (integer) | 用户产少的警告信息。 类似 E_DEPRECATED, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 | since PHP 5.3.0 |
30719 | E_ALL (integer) | E_STRICT出外的所有错误和警告信息。 | 30719 in PHP 5.3.x, 6143 in PHP 5.2.x, 2047 previously |
其中比较有价值和常见的主要有warning、notice、deprecated和fetal error几种。
deprecated是最低级的错误,一般指使用了不推荐过期的函数或语法,比如正则表达式中使用ereg开头的处理函数,忽略一般不会有什么问题,但是在环境版本升级时会很蛋疼,建议还是不要使用过时的函数。
notice是通知级别的错误,一般发生在变量未初始化、数组元素键名没有加引号等语法问题,这种错误不会影响PHP脚本的运行,但建议还是避免发生这种错误。
warning是警告级别的错误,这种错误属于比较严重的了,比如函数参数传递不全等,必须正视。
fetal error是更高级别的错误,这种错误会直接导致脚本执行终结,后面的代码都不再执行,必须有效解决。
parse error语法解析错误是最高级别的错误,上述的错误都属于PHP脚本运行阶段错误,而语法解析错误属于语法检查阶段错误,这将导致PHP代码无法通过语法检查。
(2)php.ini中对错误的设置
是否显示错误:display_errors = On/Off;
报错级别:error_reporting = E_ALL | E_STRICT; (最严格的报错级别,建议开发时使用)
在正式生产环境中,不想暴露错误等敏感信息,可以使用error_reporting(0),或者直接修改配置文件设置display_errors = Off,或者在要屏蔽错误的语句前加上@,全局错误的屏蔽还是采取前两种方法处理更好。
(3)使用set_error_handler()实现设置自定义错误处理函数
原型:
set_error_handler(error_function, error_types)
参数:
error_function为自定义的错误处理函数名,error_types为显示用户错误的报错级别。
error_function会有四个参数,分别是错误级别的整数值、错误信息、错误文件、错误发生的行数。一般的做法是在错误处理函数里对出错信息进行格式化处理,然后显示给开发者,如果是正式环境的错误,则应该记录在错误日志中,以便技术人员排查错误。
注意: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT都不能用自定义的错误处理函数来处理。
使用示例:
<?php
header("Content-Type:text/html; charset=UTF-8");
function customError($errno, $errstr, $errfile, $errline){
echo "<b>错误代码:</b>[${errno}] ${errstr}\r<br/>";
echo "错误所在的代码行:{$errline} 文件{$errfile}\r<br/>";
echo "PHP版本 ",PHP_VERSION,"(",PHP_OS,")\r<br/>";
}
set_error_handler("customError",E_ALL|E_STRICT);
$a = array('o'=>233);
echo $a[o];
使用错误处理机制抛出异常实现针对性补救:
<?php
header("Content-Type:text/html; charset=UTF-8");
function customError($errno, $errstr, $errfile, $errline){
throw new Exception(${$errstr});
}
set_error_handler('customError',E_ALL | E_STRICT);
try{
$a = 5/0;
}catch(Exception $e)
{
echo '错误信息:',$e->getMessage();
}
对于致命性的导致脚本停止运行的错误,可以使用register_shundown_function()记录日志:
<?php
class Shutdown
{
public function stop()
{
if(error_get_last())
{
echo "<pre>";
var_dump(error_get_last());
}
die('Stop.');
}
}
register_shutdown_function(array(new Shutdown(),'stop'));
$a = new a(); // 将因为致命错误而失败
echo '必须终止';
对于语法解析错误,只能设置配置文件将错误记录进日志中:
log_errors = On
error_log = xxx // 设置错误日志文件目录
五、如何在自定义的框架中整合PHP的异常和错误机制
一个良好的框架必备完善的错误和异常机制,让开发者做到有效监控管理,并且这对高发的错误和异常做出相应的优化,下面简单演示一下如何在一个PHP框架中使用错误和异常机制。
(1)错误
我们希望在开发程序的时候尽可能多的显示错误,这有助于开发的程序更加健壮。而在发布阶段,我们希望尽量避免甚至完全屏蔽错误,以免让用户看到某些敏感的信息,有些敏感信息会给黑客带来可趁之机。比较正确的做法,设置一个常量DEBUG,当其设为真或非零时,开启最严格的报错级别;反之设置系统不报错,用过多种框架的童鞋都会发现框架中都有类似于这样的一个设置,开启了debug就能看到报错,关闭则什么都不显示,原理是一样的。
当然像语法错误,致命错误这种错误不管错误级别怎么设置都会显示的,因为这几种错误是必须解决的。下面演示如何在框架中设置错误级别,格式化输出错误以及开发环境的转换。
<?php
/**
* Created by PhpStorm.
* User: ahao
* Date: 2016/12/27
* Time: 18:48
*/
define('PATH_ROOT', dirname(dirname(__FILE__)));
define('PATH_LOG', PATH_ROOT.'/log');
define('DEBUG', true);
// 根据DEBUG常量设置报错级别
if(DEBUG){
error_reporting(E_ALL | E_STRICT);
}else{
ini_set('display_errors', 0);
error_reporting(0);
}
// 设置error_handler函数接管PHP的错误处理
set_error_handler('error_handler');
function error_handler($code, $msg, $file, $line){
// 错误信息
$errorArray = compact('code', 'msg', 'file', 'line');
// 调试错误表
$errCodes = array(
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
E_NOTICE => 'NOTICE'
);
// 得到错误类型
$errorArray['type'] = isset($errCodes[$code]) ? $errCodes[$code] : 'OTHER';
// 显示跟踪信息
$traces = debug_backtrace();
foreach ($traces as $v)
{
// 错误可能发生在内存
if(!isset($v['file'])){
$trace_info = 'error not in file,maybe in memory!';
}else{
// 取得相对路径
$v['file'] = str_replace(PATH_ROOT, "", $v['file']);
// 判断错误是否在类中发生
if(isset($v['class'])){
$trace_info = "{$v['file']}, {$v['line']}, {$v['class']}, {$v['function']}";
}else if(isset($v['function'])){
$trace_info = "{$v['file']}, {$v['line']}, {$v['function']}";
}else{
$trace_info = "{$v['file']}, {$v['line']}";
}
// 判断发生错误的地方是否有参数
if(isset($v['args'])){
if(is_array($v['args'])){
foreach ($v['args'] as $ki => $vi) {
// 判断数组元素是否对象
if(is_object($vi)){
unset($vi['args'][$ki]);
$v['args'][$ki] = gettype($vi)." obj";
}
// 判断元素是否数组
else if(is_array($vi)){
unset($v['args'][$ki]);
$v['args'][$ki] = $ki." array";
}
}
// 经过上面的处理数组剩下的元素都是基本类型的了
$trace_info .= '(' . implode(',', $v['args']) . ')';
}else{
$trace_info .= '(' . $v['args'] . ')';
}
}else if(isset($v['function'])){
$trace_info .= '()';
}
}
$errorArray['trace'][] = $trace_info;
}
// 将错误信息保存在全局变量中
$GLOBALS['sys']['errorInfo'][] = $errorArray;
// 或者可以选择把错误信息输出的错误日志文件中
$handle = fopen(PATH_LOG.'/error/'.date('Y-m-d').'.log', 'a');
foreach ($errorArray['trace'] as $v){
fwrite($handle, $v."\n");
fclose($handle);
}
}
$a = 5/0;
echo "<pre>";
var_dump($GLOBALS['sys']['errorInfo']);
PS.错误日志的输出目录根据个人习惯自由设置,要检查日志目录是否存在,不存在则先生成目录再生成日志。
(2)异常
对于异常,很多框架都有内置的异常类的封装,对不同的异常进行分类处理,这里简单使用异常代号来对异常进行分类,这样我们一看代号,就知道出了那些类型的错误。同样的,我们希望不把异常直接显示到前台,而是保存到日志中,需要用自定义异常处理函数来接管,通过DEBUG常量来控制显示到网页还是记录进日志。
<?php
/**
* Created by PhpStorm.
* User: ahao
* Date: 2016/12/28
* Time: 15:10
*/
define('PATH_ROOT', dirname(dirname(__FILE__)));
define('PATH_LOG', PATH_ROOT.'/log');
define('DEBUG', true);
// 定义系统级异常的错误码
define('EXCEPTION_CORE', -100); // 框架内核异常,如加载不存在的文件异常
define('EXCEPTION_CONFIG', -101); // 加载配置异常
define('EXCEPTION_DB', -102); // 数据库语句异常
define('EXCEPTION_CACHE', -103); // 链接CACHE异常
define('EXCEPTION_API', -104); // 以PHP连接API接口异常
// 自定义异常处理函数
set_exception_handler('exception_handler');
// 格式化异常跟踪信息
function getTrace($traces){
foreach ($traces as $v){
if(!isset($v['file'])){
$trace_info = 'error not in file,maybe in memory!';
}else{
// 取得相对路径
$v['file'] = str_replace(PATH_ROOT, "", $v['file']);
if(isset($v['class'])){
$trace_info = "{$v['file']}, {$v['line']}, {$v['class']}, {$v['function']}";
}else if(isset($v['function'])){
$trace_info = "{$v['file']}, {$v['line']}, {$v['function']}";
}else{
$trace_info = "{$v['file']}, {$v['line']}";
}
// 判断发生错误的地方是否有参数
if(isset($v['args'])){
if(is_array($v['args'])){
foreach ($v['args'] as $ki => $vi) {
// 判断数组元素是否对象
if(is_object($vi)){
unset($vi['args'][$ki]);
$v['args'][$ki] = gettype($vi)." obj";
}
// 判断元素是否数组
else if(is_array($vi)){
unset($v['args'][$ki]);
$v['args'][$ki] = $ki." array";
}
}
// 经过上面的处理数组剩下的元素都是基本类型的了
$trace_info .= '(' . implode(',', $v['args']) . ')';
}else{
$trace_info .= '(' . $v['args'] . ')';
}
}else if(isset($v['function'])){
$trace_info .= '()';
}
}
$result[] = $trace_info;
}
}
function exception_handler(Exception $e){
if($e->getCode() == 404 && !DEBUG){
header("HTTP/1.0 404 Not Found");
exit;
}
$data = array();
$data['_msg'] = $e->getMessage();
$data['_code'] = $e->getCode();
$data['_trace'] = getTrace($e->getTrace());
$data['_file'] = $e->getFile();
$data['_line'] = $e->getLine();
// 调试直接显示
if(DEBUG){
echo "<pre>";
var_dump($data);
}else{
$handle = fopen(PATH_LOG.'/exception/'.date('Y-m-d').'.log', 'a');
$output = '';
foreach ($data as $k => $v){
if($k != '_trace'){
$output .= $k.": ".$v."\n";
}else{
if($v){
foreach ($v as $k2 => $v2){
$output .= "[tarce{$k2}]: ".$v2."\n";
}
}
}
}
fwrite($handle, $output);
fclose($handle);
}
}
throw new Exception('异常测试');
以上代码可以直接运行,注意设置好根目录和日志目录。
在下一篇文章,会介绍如何DIY属于你自己的PHP框架,本文介绍的内容就能够用上了23333。
这里是华农人的编程社区,欢迎关注SCAU-码农之家!