PHP开发及常用框架中异常错误处理机制
在开发的过程当中肯定会遇到报错,遇到各种级别的error或者warning。如果是php脚本会直接报错到输出,如果是框架一般会有自定义的一套错误异常机制。
laravel的
Thinkphp的
原生的
框架现在已经做的比较好了,一般会提供:函数调用栈,环境变量,具体的错误代码,具体都是怎么实现的,这里就涉及到php的错误处理函数了
如下:
https://www.php.net/manual/zh/ref.errorfunc.php#ref.errorfunc
- debug_backtrace — 产生一条回溯跟踪(backtrace)
- debug_print_backtrace — 打印一条回溯。
- error_clear_last — 清除最近一次错误
- error_get_last — 获取最后发生的错误
- error_log — 发送错误信息到某个地方
- error_reporting — 设置应该报告何种 PHP 错误
- restore_error_handler — 还原之前的错误处理函数
- restore_exception_handler — 恢复之前定义过的异常处理函数。
- set_error_handler — 设置用户自定义的错误处理函数
- set_exception_handler — 设置用户自定义的异常处理函数
- trigger_error — 产生一个用户级别的 error/warning/notice 信息
- user_error — trigger_error 的别名
首先用
set_error_handler — 设置用户自定义的错误处理函数
set_exception_handler — 设置用户自定义的异常处理函数
接受错误及异常的(如果是文件编译错误是,无法执行的等 是无法接受的)
然后
restore_error_handler — 还原之前的错误处理函数
restore_exception_handler — 恢复之前定义过的异常处理函数。
这个可有可没有,如果是组件形式的一般是在自己的组件内自定义自己的错误处理函数,保证本模块的错误异常。然后恢复不影响系统其他部分的模块。
错误内容主要的函数(debug_backtrace)
debug_backtrace — 产生一条回溯跟踪(backtrace)
debug_print_backtrace — 打印一条回溯
返回所有的调用栈这里具体的函数请参考官方文档很详细了
一般框架还有环境变量($_GET,$_POST,$_SERVER等)
这里引用laravel的报错/vendor/filp/whoops/src/Whoops 可以参考下里面涉及到的内容
public function handle() { if (!$this->handleUnconditionally()) { // Check conditions for outputting HTML: // @todo: Make this more robust if (PHP_SAPI === 'cli') { // Help users who have been relying on an internal test value // fix their code to the proper method if (isset($_ENV['whoops-test'])) { throw new \Exception( 'Use handleUnconditionally instead of whoops-test' .' environment variable' ); } return Handler::DONE; } } $templateFile = $this->getResource("views/layout.html.php"); $cssFile = $this->getResource("css/whoops.base.css"); $zeptoFile = $this->getResource("js/zepto.min.js"); $prettifyFile = $this->getResource("js/prettify.min.js"); $clipboard = $this->getResource("js/clipboard.min.js"); $jsFile = $this->getResource("js/whoops.base.js"); if ($this->customCss) { $customCssFile = $this->getResource($this->customCss); } $inspector = $this->getInspector(); $frames = $this->getExceptionFrames(); $code = $this->getExceptionCode(); // List of variables that will be passed to the layout template. $vars = [ "page_title" => $this->getPageTitle(), // @todo: Asset compiler "stylesheet" => file_get_contents($cssFile), "zepto" => file_get_contents($zeptoFile), "prettify" => file_get_contents($prettifyFile), "clipboard" => file_get_contents($clipboard), "javascript" => file_get_contents($jsFile), // Template paths: "header" => $this->getResource("views/header.html.php"), "header_outer" => $this->getResource("views/header_outer.html.php"), "frame_list" => $this->getResource("views/frame_list.html.php"), "frames_description" => $this->getResource("views/frames_description.html.php"), "frames_container" => $this->getResource("views/frames_container.html.php"), "panel_details" => $this->getResource("views/panel_details.html.php"), "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), "panel_left" => $this->getResource("views/panel_left.html.php"), "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), "frame_code" => $this->getResource("views/frame_code.html.php"), "env_details" => $this->getResource("views/env_details.html.php"), "title" => $this->getPageTitle(), "name" => explode("\\", $inspector->getExceptionName()), "message" => $inspector->getExceptionMessage(), "previousMessages" => $inspector->getPreviousExceptionMessages(), "docref_url" => $inspector->getExceptionDocrefUrl(), "code" => $code, "previousCodes" => $inspector->getPreviousExceptionCodes(), "plain_exception" => Formatter::formatExceptionPlain($inspector), "frames" => $frames, "has_frames" => !!count($frames), "handler" => $this, "handlers" => $this->getRun()->getHandlers(), "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', "has_frames_tabs" => $this->getApplicationPaths(), "tables" => [ "GET Data" => $this->masked($_GET, '_GET'), "POST Data" => $this->masked($_POST, '_POST'), "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], "Cookies" => $this->masked($_COOKIE, '_COOKIE'), "Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [], "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), "Environment Variables" => $this->masked($_ENV, '_ENV'), ], ]; if (isset($customCssFile)) { $vars["stylesheet"] .= file_get_contents($customCssFile); } // Add extra entries list of data tables: // @todo: Consolidate addDataTable and addDataTableCallback $extraTables = array_map(function ($table) use ($inspector) { return $table instanceof \Closure ? $table($inspector) : $table; }, $this->getDataTables()); $vars["tables"] = array_merge($extraTables, $vars["tables"]); $plainTextHandler = new PlainTextHandler(); $plainTextHandler->setException($this->getException()); $plainTextHandler->setInspector($this->getInspector()); $vars["preface"] = "<!--\n\n\n" . $this->templateHelper->escape($plainTextHandler->generateResponse()) . "\n\n\n\n\n\n\n\n\n\n\n-->"; $this->templateHelper->setVariables($vars); $this->templateHelper->render($templateFile); return Handler::QUIT; }
然后我这边做了一个小例子分别触发报错跟告警,然后自定义接受
目录
一共有A,B,C三个类
index.php设置错误异常处理机制,然后引用include_func执行具体的代码
A调用B调用C,common.php这个主要是美化debug_backtrace的可以忽略,根据自己的具体需求进行美化
A
<?php include_once 'B.class.php'; class A { private $handle; public function __construct() { $this->handle = new B(); } function triggerError() { return $this->handle->triggerError(); } function throwException() { return $this->handle->throwException(); } }
B
<?php include_once 'C.class.php'; class B { private $handle; public function __construct() { $this->handle = new C(); } function triggerError() { return $this->handle->triggerError(); } function throwException() { return $this->handle->throwException(); } }
C
<?php class C { function triggerError() { trigger_error("this is an error", E_USER_ERROR); } function throwException() { throw new \Exception("this is an exception"); } }
index.php
<?php include_once 'common.php'; function myErrorHandler($errno, $errstr, $errfile, $errline) { echo "<b>My ERROR</b> [$errno] $errstr<br />\n"; echo " Fatal error on line $errline in file $errfile"; echo ", PHP " . PHP_VERSION . " (" . PHP_OS . ")<br />\n"; echo "Aborting...<br />\n"; $traces = debug_backtrace(false); formatBackTrace($traces); exit(1); } function myExceptionHandler(Exception $exception) { echo "Uncaught exception: ", $exception->getMessage(), "\n"; $traces = $exception->getTrace(); formatBackTrace($traces); exit(1); } function formatBackTrace($traces) { echo "<hr/>\r\n"; foreach ($traces as $trace) { // Show Function if($trace['function']){ echo sprintf( 'at %s%s%s(%s)', isset($trace['class']) ? parse_class($trace['class']) : '', isset($trace['type']) ? $trace['type'] : '', $trace['function'], isset($trace['args'])?parse_args($trace['args']):'' ); } // Show line if (isset($trace['file']) && isset($trace['line'])) { echo sprintf(' in %s', parse_file($trace['file'], $trace['line'])); } echo "<br/>"; } echo "<hr/>\r\n"; } set_error_handler('myErrorHandler'); set_exception_handler('myExceptionHandler'); //do something being include_once 'include_func.php'; //do something end restore_error_handler(); restore_exception_handler();
include_func.php
<?php include_once 'A.class.php'; $a = new A(); $a->throwException(); if (rand(1, 10) > 5) { } else { //$a->triggerError(); }
common.php
<?php if(!function_exists('parse_padding')){ function parse_padding($source) { $length = strlen(strval(count($source['source']) + $source['first'])); return 40 + ($length - 1) * 8; } } if(!function_exists('parse_class')){ function parse_class($name) { $names = explode('\\', $name); return '<abbr title="'.$name.'">'.end($names).'</abbr>'; } } if(!function_exists('parse_file')){ function parse_file($file, $line) { return '<a class="toggle" title="'."{$file} line {$line}".'">'.basename($file)." line {$line}".'</a>'; } } if(!function_exists('parse_args')){ function parse_args($args) { $result = []; foreach ($args as $key => $item) { switch (true) { case is_object($item): $value = sprintf('<em>object</em>(%s)', parse_class(get_class($item))); break; case is_array($item): if(count($item) > 3){ $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); } else { $value = sprintf('[%s]', parse_args($item)); } break; case is_string($item): if(strlen($item) > 20){ $value = sprintf( '\'<a class="toggle" title="%s">%s</a>\'', htmlentities($item), htmlentities(substr($item, 0, 1500)) ); } else { $value = sprintf("'%s'", htmlentities($item)); } break; case is_int($item): case is_float($item): $value = $item; break; case is_null($item): $value = '<em>null</em>'; break; case is_bool($item): $value = '<em>' . ($item ? 'true' : 'false') . '</em>'; break; case is_resource($item): $value = '<em>resource</em>'; break; default: $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); break; } $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; } return implode(', ', $result); } } function parse_file($file, $line) { return '<a class="toggle" title="'."{$file} line {$line}".'">'.$file." line {$line}".'</a>'; }
触发error
触发exception
具体的美化可以根据个人项目框架实施。基本上在php中的错误机制就是大概就是上面的步骤。自定义异常接受函数,然后处理错误信息,进行不同面板的展示或者日志记录。
下面是C的源码 基本就是调用栈的反向遍历
ZEND_API void zend_fetch_debug_backtrace(zval *return_value, int skip_last, int options, int limit) /* {{{ */ { zend_execute_data *ptr, *skip, *call = NULL; zend_object *object; int lineno, frameno = 0; zend_function *func; zend_string *function_name; zend_string *filename; zend_string *include_filename = NULL; zval stack_frame, tmp; array_init(return_value); if (!(ptr = EG(current_execute_data))) { return; } if (!ptr->func || !ZEND_USER_CODE(ptr->func->common.type)) { call = ptr; ptr = ptr->prev_execute_data; } if (ptr) { if (skip_last) { /* skip debug_backtrace() */ call = ptr; ptr = ptr->prev_execute_data; } else { /* skip "new Exception()" */ if (ptr->func && ZEND_USER_CODE(ptr->func->common.type) && (ptr->opline->opcode == ZEND_NEW)) { call = ptr; ptr = ptr->prev_execute_data; } } if (!call) { call = ptr; ptr = ptr->prev_execute_data; } } while (ptr && (limit == 0 || frameno < limit)) { frameno++; array_init(&stack_frame); ptr = zend_generator_check_placeholder_frame(ptr); skip = ptr; /* skip internal handler */ if (skip_internal_handler(skip)) { skip = skip->prev_execute_data; } if (skip->func && ZEND_USER_CODE(skip->func->common.type)) { filename = skip->func->op_array.filename; if (skip->opline->opcode == ZEND_HANDLE_EXCEPTION) { if (EG(opline_before_exception)) { lineno = EG(opline_before_exception)->lineno; } else { lineno = skip->func->op_array.line_end; } } else { lineno = skip->opline->lineno; } ZVAL_STR_COPY(&tmp, filename); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_FILE), &tmp); ZVAL_LONG(&tmp, lineno); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_LINE), &tmp); /* try to fetch args only if an FCALL was just made - elsewise we're in the middle of a function * and debug_baktrace() might have been called by the error_handler. in this case we don't * want to pop anything of the argument-stack */ } else { zend_execute_data *prev_call = skip; zend_execute_data *prev = skip->prev_execute_data; while (prev) { if (prev_call && prev_call->func && !ZEND_USER_CODE(prev_call->func->common.type) && !(prev_call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) { break; } if (prev->func && ZEND_USER_CODE(prev->func->common.type)) { ZVAL_STR_COPY(&tmp, prev->func->op_array.filename); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_FILE), &tmp); ZVAL_LONG(&tmp, prev->opline->lineno); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_LINE), &tmp); break; } prev_call = prev; prev = prev->prev_execute_data; } filename = NULL; } /* $this may be passed into regular internal functions */ object = (call && (Z_TYPE(call->This) == IS_OBJECT)) ? Z_OBJ(call->This) : NULL; if (call && call->func) { func = call->func; function_name = (func->common.scope && func->common.scope->trait_aliases) ? zend_resolve_method_name( (object ? object->ce : func->common.scope), func) : func->common.function_name; } else { func = NULL; function_name = NULL; } if (function_name) { ZVAL_STR_COPY(&tmp, function_name); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_FUNCTION), &tmp); if (object) { if (func->common.scope) { ZVAL_STR_COPY(&tmp, func->common.scope->name); } else if (object->handlers->get_class_name == zend_std_get_class_name) { ZVAL_STR_COPY(&tmp, object->ce->name); } else { ZVAL_STR(&tmp, object->handlers->get_class_name(object)); } zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_CLASS), &tmp); if ((options & DEBUG_BACKTRACE_PROVIDE_OBJECT) != 0) { ZVAL_OBJ(&tmp, object); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_OBJECT), &tmp); Z_ADDREF(tmp); } ZVAL_INTERNED_STR(&tmp, ZSTR_KNOWN(ZEND_STR_OBJECT_OPERATOR)); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_TYPE), &tmp); } else if (func->common.scope) { ZVAL_STR_COPY(&tmp, func->common.scope->name); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_CLASS), &tmp); ZVAL_INTERNED_STR(&tmp, ZSTR_KNOWN(ZEND_STR_PAAMAYIM_NEKUDOTAYIM)); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_TYPE), &tmp); } if ((options & DEBUG_BACKTRACE_IGNORE_ARGS) == 0 && func->type != ZEND_EVAL_CODE) { debug_backtrace_get_args(call, &tmp); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_ARGS), &tmp); } } else { /* i know this is kinda ugly, but i'm trying to avoid extra cycles in the main execution loop */ zend_bool build_filename_arg = 1; zend_string *pseudo_function_name; if (!ptr->func || !ZEND_USER_CODE(ptr->func->common.type) || ptr->opline->opcode != ZEND_INCLUDE_OR_EVAL) { /* can happen when calling eval from a custom sapi */ pseudo_function_name = ZSTR_KNOWN(ZEND_STR_UNKNOWN); build_filename_arg = 0; } else switch (ptr->opline->extended_value) { case ZEND_EVAL: pseudo_function_name = ZSTR_KNOWN(ZEND_STR_EVAL); build_filename_arg = 0; break; case ZEND_INCLUDE: pseudo_function_name = ZSTR_KNOWN(ZEND_STR_INCLUDE); break; case ZEND_REQUIRE: pseudo_function_name = ZSTR_KNOWN(ZEND_STR_REQUIRE); break; case ZEND_INCLUDE_ONCE: pseudo_function_name = ZSTR_KNOWN(ZEND_STR_INCLUDE_ONCE); break; case ZEND_REQUIRE_ONCE: pseudo_function_name = ZSTR_KNOWN(ZEND_STR_REQUIRE_ONCE); break; default: /* this can actually happen if you use debug_backtrace() in your error_handler and * you're in the top-scope */ pseudo_function_name = ZSTR_KNOWN(ZEND_STR_UNKNOWN); build_filename_arg = 0; break; } if (build_filename_arg && include_filename) { zval arg_array; array_init(&arg_array); /* include_filename always points to the last filename of the last last called-function. if we have called include in the frame above - this is the file we have included. */ ZVAL_STR_COPY(&tmp, include_filename); zend_hash_next_index_insert_new(Z_ARRVAL(arg_array), &tmp); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_ARGS), &arg_array); } ZVAL_INTERNED_STR(&tmp, pseudo_function_name); zend_hash_add_new(Z_ARRVAL(stack_frame), ZSTR_KNOWN(ZEND_STR_FUNCTION), &tmp); } zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &stack_frame); include_filename = filename; call = skip; ptr = skip->prev_execute_data; } } /* }}} */ /* {{{ proto array debug_backtrace([int options[, int limit]]) Return backtrace as array */ ZEND_FUNCTION(debug_backtrace) { zend_long options = DEBUG_BACKTRACE_PROVIDE_OBJECT; zend_long limit = 0; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|ll", &options, &limit) == FAILURE) { return; } zend_fetch_debug_backtrace(return_value, 1, options, limit); }
debug_print_backtrace:这个跟上面类似 只是没有返回直接调用print 打印输出了
ZEND_FUNCTION(debug_print_backtrace) { zend_execute_data *call, *ptr, *skip; zend_object *object; int lineno, frameno = 0; zend_function *func; const char *function_name; const char *filename; zend_string *class_name = NULL; char *call_type; const char *include_filename = NULL; zval arg_array; int indent = 0; zend_long options = 0; zend_long limit = 0; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|ll", &options, &limit) == FAILURE) { return; } ZVAL_UNDEF(&arg_array); ptr = EX(prev_execute_data); /* skip debug_backtrace() */ call = ptr; ptr = ptr->prev_execute_data; while (ptr && (limit == 0 || frameno < limit)) { frameno++; class_name = NULL; call_type = NULL; ZVAL_UNDEF(&arg_array); ptr = zend_generator_check_placeholder_frame(ptr); skip = ptr; /* skip internal handler */ if (skip_internal_handler(skip)) { skip = skip->prev_execute_data; } if (skip->func && ZEND_USER_CODE(skip->func->common.type)) { filename = ZSTR_VAL(skip->func->op_array.filename); if (skip->opline->opcode == ZEND_HANDLE_EXCEPTION) { if (EG(opline_before_exception)) { lineno = EG(opline_before_exception)->lineno; } else { lineno = skip->func->op_array.line_end; } } else { lineno = skip->opline->lineno; } } else { filename = NULL; lineno = 0; } /* $this may be passed into regular internal functions */ object = (Z_TYPE(call->This) == IS_OBJECT) ? Z_OBJ(call->This) : NULL; if (call->func) { zend_string *zend_function_name; func = call->func; if (func->common.scope && func->common.scope->trait_aliases) { zend_function_name = zend_resolve_method_name(object ? object->ce : func->common.scope, func); } else { zend_function_name = func->common.function_name; } if (zend_function_name != NULL) { function_name = ZSTR_VAL(zend_function_name); } else { function_name = NULL; } } else { func = NULL; function_name = NULL; } if (function_name) { if (object) { if (func->common.scope) { class_name = func->common.scope->name; } else if (object->handlers->get_class_name == zend_std_get_class_name) { class_name = object->ce->name; } else { class_name = object->handlers->get_class_name(object); } call_type = "->"; } else if (func->common.scope) { class_name = func->common.scope->name; call_type = "::"; } else { class_name = NULL; call_type = NULL; } if (func->type != ZEND_EVAL_CODE) { if ((options & DEBUG_BACKTRACE_IGNORE_ARGS) == 0) { debug_backtrace_get_args(call, &arg_array); } } } else { /* i know this is kinda ugly, but i'm trying to avoid extra cycles in the main execution loop */ zend_bool build_filename_arg = 1; if (!ptr->func || !ZEND_USER_CODE(ptr->func->common.type) || ptr->opline->opcode != ZEND_INCLUDE_OR_EVAL) { /* can happen when calling eval from a custom sapi */ function_name = "unknown"; build_filename_arg = 0; } else switch (ptr->opline->extended_value) { case ZEND_EVAL: function_name = "eval"; build_filename_arg = 0; break; case ZEND_INCLUDE: function_name = "include"; break; case ZEND_REQUIRE: function_name = "require"; break; case ZEND_INCLUDE_ONCE: function_name = "include_once"; break; case ZEND_REQUIRE_ONCE: function_name = "require_once"; break; default: /* this can actually happen if you use debug_backtrace() in your error_handler and * you're in the top-scope */ function_name = "unknown"; build_filename_arg = 0; break; } if (build_filename_arg && include_filename) { array_init(&arg_array); add_next_index_string(&arg_array, (char*)include_filename); } call_type = NULL; } zend_printf("#%-2d ", indent); if (class_name) { ZEND_PUTS(ZSTR_VAL(class_name)); ZEND_PUTS(call_type); if (object && !func->common.scope && object->handlers->get_class_name != zend_std_get_class_name) { zend_string_release_ex(class_name, 0); } } zend_printf("%s(", function_name); if (Z_TYPE(arg_array) != IS_UNDEF) { debug_print_backtrace_args(&arg_array); zval_ptr_dtor(&arg_array); } if (filename) { zend_printf(") called at [%s:%d]\n", filename, lineno); } else { zend_execute_data *prev_call = skip; zend_execute_data *prev = skip->prev_execute_data; while (prev) { if (prev_call && prev_call->func && !ZEND_USER_CODE(prev_call->func->common.type)) { prev = NULL; break; } if (prev->func && ZEND_USER_CODE(prev->func->common.type)) { zend_printf(") called at [%s:%d]\n", ZSTR_VAL(prev->func->op_array.filename), prev->opline->lineno); break; } prev_call = prev; prev = prev->prev_execute_data; } if (!prev) { ZEND_PUTS(")\n"); } } include_filename = filename; call = skip; ptr = skip->prev_execute_data; ++indent; } }
这里关于c的源码不做过多解释,其实就是逆向遍历调用栈。参考使用
从现在开始博客不能那么水了,不能仅仅是烂笔头的复制粘贴存档的地方。要知其然及其所以然。告诉自己你已经是个成熟的程序员了,不再是新手了不能再curd了!否则35岁以后不好走啊