你知道PHP中Exception, Error Handler的这些细节吗?

前言

最近项目中有一个功能需要实现:
调试模式下, 将所有错误提前输出, 再输出页面内容.

为实现上述功能, 需使用到Exception, Error相关Handler方法, 发现有许多坑, 故写此文与大家分享.

主要函数

此篇文章重点关注以下几个函数

  • error_reporting()
  • set_error_handler()
  • set_exception_handler()
  • register_shutdown_function()
  • error_get_last()

这有什么难的?

哈~ 如果您现在有标题中的感慨, 那么也请关注以下本文中将重点讲述的问题列表:

  1. error_reporting()error_get_last() 有什么联系?
  2. set_error_handler()set_exception_handler() 绑定的handler什么时候才会启动? 它们有什么联系?
  3. register_shutdown_function()通常跟Exception/Error有关系么?

上述问题描述模糊, 因此答案也可能千人千面.
因此, 本文只给出自己的答案与大家分享, 如有问题或不同的见解, 期待与您沟通.
如果以上问题, 并不能引起您的兴趣, 或者您已理解透彻了, 就可以自行右上角小红叉啦~

解疑:

1. error_reporting()error_get_last() 有什么联系?

link: php.net - error_reporting()
link: php.net - error_get_last()

  • int error_reporting ([ int $level ] )
    大家应该再熟悉不过了, 因此不再赘述.

  • array error_get_last ( void )

    获取最后发生的错误.

    通常用来获取PHP运行过程中的Fatal Error错误(PHP 5).

这两个函数在字面上关联性并不强, 但请观察以下代码及输出

<?php
error_reporting(E_ALL & ~E_NOTICE);
$a = $b;  //E_NOTICE
print_r(error_get_last());

/* output:
Array
(
    [type] => 8
    [message] => Undefined variable: b
    [file] => /app/t.php
    [line] => 3
)
*/

error_get_last()虽然说明了获取最后发生的错误, 实际上也是如此. 但却没有说明, 被error_reporting()忽略掉的错误是否有可能被获取到, 因此, 当我们使用error_get_last()时需要注意我平时忽略掉的错误, 如: E_DEPRECATED

2. set_error_handler()set_exception_handler() 绑定的handler什么时候才会启动? 它们有什么联系?

link: php.net - set_error_handler()
link: php.net - set_exception_handler()

  • mixed set_error_handler ( callable $error_handler [, int $error_types = E_ALL | E_STRICT ] )

    设置用户自定义的错误处理函数.

    通常在PHP脚本运行过程中, 出现一些非中断性错误时触发.
    我们会用这个来记录错误日志或直接输出等操作.
    注意:

    1. 参数$error_types大多设定为error_reporting(), 但建议设定为E_ALL, 具体哪些错误需要被处理, 哪些不需要, 在handler内进行判断明显更加灵活.

    2. 以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT

    3. handler被触发后, 并不会中断PHP运行.

    4. bool error_handler ( int $errno , string $errstr [, string $errfile [, int $errline [, array $errcontext ]]] )
      注意error_handler的返回值:

      • FALSE: 标准的错误处理依然会被执行(标准错误处理根据 display_errors = true/false 决定是否输出到stderr)
  • callable set_exception_handler ( callable $exception_handler )

    设置用户自定义的异常处理函数
    设置默认的异常处理程序,用于没有用 try/catch 块来捕获的异常。 在 exception_handler 调用后异常会中止。

    注意:

    1. exception_handler 调用后异常会中止(脚本终止).
    2. PHP 5, PHP 7exception_handler并不相同.
      PHP 5: void handler ( Exception $ex )
      PHP 7: void handler ( Throwable $ex )
    3. 自 PHP 7 以来,大多数错误抛出 Error 异常,也能被捕获。 Error 和 Exception 都实现了 Throwable 接口。

    注意点中2, 3项轻描淡写了一下PHP 5/PHP 7之间的不同却透露出重要的消息(坑..)
    PHP 7中, exception_handler 不再只接受Exception了, 并且接收了Error错误.
    link: php.net - PHP7 Errors列表

因此, set_error_handler()set_exception_handler() 之间的关系也迎刃而解:

  • PHP 5:

    • set_error_handler(): 负责非中断行错误.
    • set_exception_handler(): 负责没有被catch的异常(会中断).
    • Fatal Error等: 并不会被两者管理, 正常输出到屏幕上(弊端).
  • PHP 7:

    • set_error_handler(): 负责非中断行错误.
    • set_exception_handler(): 负责没有被catch的异常, Error(会中断)
    • Fatal Error等: 由set_exception_handler()管理.

3. register_shutdown_function()通常跟Exception/Error有关系么?

link: php.net - register_shutdown_function()

注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用。

根据说明可以得出结论, 它与Exception/Error完全没关系.
提出这个问题, 主要是因为, 在PHP5Fatal Error并没有明确的接收地点, 所以我们通常配合error_get_last()来接收Fatal Error

<?php 
register_shutdown_function('shutdown_function');
unknown_function();

function shutdown_function() {
  print_r(error_get_last());
}

/* output:
Array
(
    [type] => 1
    [message] => Uncaught Error: Call to undefined function unknown_function() in /app/t.php:3
Stack trace:
#0 {main}
  thrown
    [file] => /app/t.php
    [line] => 3
)
*/

然而随着PHP 7的到来, Error已经可以被set_exception_handler()捕捉了, 再通过error_get_last()就多余了. shutdown中更多的是一些版本冗余的工作.

栗子

前言中的需求: 调试模式下, 将所有错误提前输出, 再输出页面内容.
以下是demo, 省去了环境判断(debug环境), 大家可以根据下面这段代码, 了解本文中所说的各种handler的触发和调用情况.

<?php

/*
要求: 将所有异常打印在屏幕最上方
*/

/* Fatal Error 中断脚本 -> shutdown_handler */

//设置错误级别
define("END_ERRORS", '--END ERRORS--' . PHP_EOL . PHP_EOL);
ini_set('display_errors', true);
ini_set('error_reporting', E_ALL & ~E_DEPRECATED);

set_error_handler('usr_err_handler', error_reporting()); //注册错误处理函数
set_exception_handler('usr_ex_handler'); //注册异常处理函数
register_shutdown_function('shutdown_handler');    //注册会在php中止时执行的函数


$global_errors = [];    //用于记录所有错误
$errnos = [             //错误级别
    0 => 'ERROR',//PHP7 ERROR的CODE
    1 => 'E_ERROR',//FATAL ERROR(PHP5), E_ERROR
    2 => 'E_WARNING',
    4 => 'E_PARSE',
    8 => 'E_NOTICE',
    16 => 'E_CORE_ERROR',
    32 => 'E_CORE_WARNING',
    64 => 'E_COMPILE_ERROR',
    128 => 'E_COMPILE_WARNING',
    256 => 'E_USER_ERROR',
    512 => 'E_USER_WARNING',
    1024 => 'E_USER_NOTICE',
    2048 => 'E_STRICT',
    4096 => 'E_RECOVERABLE_ERROR',
    8192 => 'E_DEPRECATED',
    16384 => 'E_USER_DEPRECATED',
    30719 => 'E_ALL',
];

function reset_errors()
{
    global $global_errors;
    $global_errors = [];
}

function get_errnostr($errno)
{
    global $errnos;
    return $errnos[$errno];
}

function set_errnos($errno, $errstr)
{
    global $global_errors;
    $global_errors[] = [
        'errno' => $errno,
        'errnostr' => get_errnostr($errno),
        'errstr' => $errstr,
    ];
}

function print_errors($prefix)
{
    global $global_errors;
    foreach ($global_errors as $err) {//由于handler中依然有可能有error 因此放最后
        printf("[%s]: %s, %d, %s\n",
            $prefix, $err['errnostr'], $err['errno'], $err['errstr']);
    }
}

//用户异常处理函数 (进来就中断脚本) PHP5只有Exception进来   PHP7Error和Exception
//PHP7中 void handler (Throwable $ex) 可捕获Error和Exception两种异常, 暂不管
//http://php.net/manual/en/language.errors.php7.php PHP7 Error阅读
//内部如果有Error则触发Error函数, 再回到错误行继续执行
function usr_ex_handler($ex)
{
    $content = ob_get_clean();  //让Exception/Error提前展示

    print_errors('EX ERROR');
    reset_errors();

    $errnostr = get_errnostr($ex->getCode());
    $errno = $ex->getCode();
    $errstr = $ex->getMessage();

    if ($ex instanceof Exception) {
        printf("[EXCEPTION]: %s, %d, %s\n", $errnostr, $errno, $errstr);
    } else {//针对PHP7  $ex instanceof Error
        printf("[EX FATAL ERROR]: %s, %d, %s\n", $errnostr, $errno, $errstr);
    }

    //由于handler中依然有可能有error 因此放最后
    print_errors('EX ERROR');
    reset_errors();

    echo END_ERRORS;
    echo $content;

    return;
}

//用户错误处理函数
//E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING不能被用户处理
function usr_err_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
    set_errnos($errno, $errstr);
    return true;    //如果函数返回 FALSE,标准错误处理处理程序将会继续调用。
}

//用户PHP终止函数
function shutdown_handler()
{
    $content = ob_get_clean();  //让Exception/Error提前展示
    $err = error_get_last();//检查一下是否有遗漏掉的错误 php5 fatal error
    if ($err['type'] & error_reporting()) {
        set_errnos($err['type'], $err['message']);
    }
    print_errors('ST ERROR');
    reset_errors();

    echo $content;
}

ob_start();

echo 'Main function...', PHP_EOL;

//搞事情
//throw new Exception('这是一个异常');
trigger_error('这是一个用户error');//E_USER_NOTICE

if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
    mcrypt_encrypt();//E_WARNING, E_DEPRECATED
} else {
    mysql();
}
unknown_function(); //fatal error


$content = ob_get_clean();

//优先输出错误
print_errors('MA ERROR');
if (!empty($global_errors)) {
    echo END_ERRORS;
}
reset_errors();

//输出正文内容
echo $content;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容