本篇博客属于php7扩展系列教程,上一篇地址《PHP7扩展开发教程[11] – 如何引入php文件?》。
本章将演示如何在扩展中抛出错误信息error,或者异常exception。
正式开始
我新增了一个全局函数用于测试error:zif_myext_test_error,它的实现如下:
1 2 3 4 5 6 7 8 9 10 |
void zif_myext_test_error(zend_execute_data *execute_data, zval *return_value) { TRACE("zif_myext_test_error"); php_error_docref(NULL, E_WARNING, "I AM %s", "WARNING"); php_error_docref2(NULL, "string $parmas1", "integer $params2", E_NOTICE, "I AM %s", "JUST A NOTICE"); // fatal error will stop executing script php_error_docref1(NULL, "string $params1", E_ERROR, "I AM %s", "FATAL ERROR"); } |
调用test_error()方法,可以看到PHP抛出的错误信息:
1 2 3 |
PHP Warning: test_error(): I AM WARNING in /home/liangdong/myext/test.php on line 17 PHP Notice: test_error(string $parmas1,integer $params2): I AM JUST A NOTICE in /home/liangdong/myext/test.php on line 17 PHP Fatal error: test_error(string $params1): I AM FATAL ERROR in /home/liangdong/myext/test.php on line 17 |
错误输出用到了系列的php_error_docref函数,它们在php源码的main/php.h中声明,在main/main.c中定义:
1 2 3 4 5 6 7 8 |
PHPAPI ZEND_COLD void php_error_docref0(const char *docref, int type, const char *format, ...) PHP_ATTRIBUTE_FORMAT(printf, 3, 4); PHPAPI ZEND_COLD void php_error_docref1(const char *docref, const char *param1, int type, const char *format, ...) PHP_ATTRIBUTE_FORMAT(printf, 4, 5); PHPAPI ZEND_COLD void php_error_docref2(const char *docref, const char *param1, const char *param2, int type, const char *format, ...) PHP_ATTRIBUTE_FORMAT(printf, 5, 6); #define php_error_docref php_error_docref0 |
docref参数用于指向文档(比如php.net上某个函数的说明),一般我们不会用。
type是错误级别,这个大家都很熟悉,其枚举定义在Zend/zend_errors.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#define E_ERROR (1<<0L) #define E_WARNING (1<<1L) #define E_PARSE (1<<2L) #define E_NOTICE (1<<3L) #define E_CORE_ERROR (1<<4L) #define E_CORE_WARNING (1<<5L) #define E_COMPILE_ERROR (1<<6L) #define E_COMPILE_WARNING (1<<7L) #define E_USER_ERROR (1<<8L) #define E_USER_WARNING (1<<9L) #define E_USER_NOTICE (1<<10L) #define E_STRICT (1<<11L) #define E_RECOVERABLE_ERROR (1<<12L) #define E_DEPRECATED (1<<13L) #define E_USER_DEPRECATED (1<<14L) #define E_ALL (E_ERROR | E_WARNING | E_PARSE | E_NOTICE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE | E_RECOVERABLE_ERROR | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT) #define E_CORE (E_CORE_ERROR | E_CORE_WARNING) |
其中ERROR相关级别是FATAL错误,会导致PHP脚本停止向下执行。所以,一般我们抛WANRING或者NOTICE就可以了。
如果你仔细观察输出的错误信息,应该可以发现param1和param2其实最终会用来格式化错误输出信息,作为函数的参数展现。
那么,php_error_docref系列函数的实现大概是什么样子呢?
1 2 3 4 5 6 7 8 |
PHPAPI ZEND_COLD void php_error_docref0(const char *docref, int type, const char *format, ...) { va_list args; va_start(args, format); php_verror(docref, "", type, format, args); va_end(args); } |
可见,它间接通过php_verror实现,这个函数比较长,其主要功能是获取当前执行上下文的函数名、类名、代码行、文件名等信息,经过一系列处理最终格式化为一个错误字符串message,然后调用了php_error进一步处理:
1 2 3 4 5 6 7 8 9 10 11 |
/* {{{ php_verror */ /* php_verror is called from php_error_docref<n> functions. * Its purpose is to unify error messages and automatically generate clickable * html error messages if correcponding ini setting (html_errors) is activated. * See: CODING_STANDARDS for details. */ PHPAPI ZEND_COLD void php_verror(const char *docref, const char *params, int type, const char *format, va_list args) { ... ... php_error(type, "%s", message); |
那么php_error其实是zend_error的一个宏名字,zend_error函数除了帮我们输出message外,还会检查PHP里是否设置过错误处理函数(set_error_handler),如果设置了就会回调,这里就不贴代码了。
可见,php_error_docref系列函数能够生成更多上下文信息,而直接调用zend_error并不会帮我们生成这些信息,因此作为一个扩展来说,一般都是使用php_error_docref而不是直接使用php_error/zend_error。
接下来是异常,在函数zif_myext_test_exception实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void zif_myext_test_exception(zend_execute_data *execute_data, zval *return_value) { TRACE("zif_myext_test_exception"); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); assert(num_args == 1 && Z_TYPE_P(args) == IS_LONG); // throw self-defined exception if (Z_LVAL_P(args) == 1) { // inherit from base exception zend_function_entry nofuncs[] = { // fname,handler,arg_info,,num_args,flags {NULL, NULL, NULL, 0, 0}, }; zend_class_entry my_exception_def; INIT_CLASS_ENTRY_EX(my_exception_def, "MyException", sizeof("MyException") - 1, nofuncs); zend_class_entry *my_exception_handle = zend_register_internal_class_ex(&my_exception_def, zend_ce_exception); assert(my_exception_handle); // throw myexception zend_throw_exception(my_exception_handle, "I AM MY EXCEPTION", 10086); } else { // throw base exception directly zend_throw_exception(NULL, "I AM DEFAULT EXCEPTION", 10087); } } |
在PHP代码里,我这样进行了测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php // default exception try { test_exception(0); } catch (Exception $e) { var_dump($e); } // self-defined exception try { test_exception(1); } catch (Exception $e) { var_dump($e); } |
当我传参数0时进入第2个分支,zend_throw_exception函数定义如下:
1 |
ZEND_API ZEND_COLD zend_object *zend_throw_exception(zend_class_entry *exception_ce, const char *message, zend_long code); |
第1个参数是异常类的class句柄,如果传NULL则表示使用默认的Exception类作为异常类型,message和code和PHP里创建Exception类一样,分别是异常提示信息和异常错误码。
这个函数会根据exception_ce创建一个新的异常对象,设置其message和code,最后帮我们抛出去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
ZEND_API ZEND_COLD zend_object *zend_throw_exception(zend_class_entry *exception_ce, const char *message, zend_long code) /* {{{ */ { zval ex, tmp; if (exception_ce) { if (!instanceof_function(exception_ce, zend_ce_throwable)) { zend_error(E_NOTICE, "Exceptions must implement Throwable"); exception_ce = zend_ce_exception; } } else { exception_ce = zend_ce_exception; } object_init_ex(&ex, exception_ce); if (message) { ZVAL_STRING(&tmp, message); zend_update_property_ex(exception_ce, &ex, CG(known_strings)[ZEND_STR_MESSAGE], &tmp); zval_ptr_dtor(&tmp); } if (code) { ZVAL_LONG(&tmp, code); zend_update_property_ex(exception_ce, &ex, CG(known_strings)[ZEND_STR_CODE], &tmp); } zend_throw_exception_internal(&ex); return Z_OBJ(ex); } |
可见,如果没有传递exception_ce就会使用默认的一个zend_ce_exception作为异常类句柄,如果传递了则会检查我们的类是否继承自异常类接口。接着,它就会创建一个object类对象,设置对象的属性,最后抛出它。
我们不需要释放这个函数的返回值,因为最终这个object是会返回到PHP层,交由用户层捕获。
在这里,我们会看到PHP里捕获的异常对象如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
object(Exception)#1 (7) { ["message":protected]=> string(22) "I AM DEFAULT EXCEPTION" ["string":"Exception":private]=> string(0) "" ["code":protected]=> int(10087) ["file":protected]=> string(30) "/home/liangdong/myext/test.php" ["line":protected]=> int(5) ["trace":"Exception":private]=> array(1) { [0]=> array(4) { ["file"]=> string(30) "/home/liangdong/myext/test.php" ["line"]=> int(5) ["function"]=> string(14) "test_exception" ["args"]=> array(1) { zif_myext_test_exception(myext.c:566) - zif_myext_test_exception [0]=> int(0) } } } ["previous":"Exception":private]=> NULL } |
接下来test_exception传1进入第1个分支,我们自定义一个新的异常类叫做MyException,令它继承自Exception类,全局变量zend_ce_exception已经被Zend初始化为Exception类的句柄,可以直接使用:
1 2 3 4 |
extern ZEND_API zend_class_entry *zend_ce_exception; /* Deprecated - Use zend_ce_exception directly instead */ ZEND_API zend_class_entry *zend_exception_get_default(void); |
我不打算覆写Exception类的方法(例如:getMessage),所以方法列表为空。最后,调用zend_throw_exception只是将第1个参数改为了我的MyException类句柄,其输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
object(MyException)#2 (7) { ["message":protected]=> string(17) "I AM MY EXCEPTION" ["string":"Exception":private]=> string(0) "" ["code":protected]=> int(10086) ["file":protected]=> string(30) "/home/liangdong/myext/test.php" ["line":protected]=> int(12) ["trace":"Exception":private]=> array(1) { [0]=> array(4) { ["file"]=> string(30) "/home/liangdong/myext/test.php" ["line"]=> int(12) ["function"]=> string(14) "test_exception" ["args"]=> array(1) { [0]=> int(1) } } } ["previous":"Exception":private]=> NULL } |
结语
通过本章,你应该掌握:
- 抛出error。
- 抛出exception。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

赞
欢迎常来~
1