diff --git a/NEWS b/NEWS index ee9ee810b2565..145a1d9a5cb56 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,9 @@ PHP NEWS . ext/bcmath: Check for scale overflow. (SakiTakamachi) . [RFC] ext/bcmath: Added bcdivmod. (SakiTakamachi) +- Curl: + . Added CURLOPT_DEBUGFUNCTION as a Curl option. (Ayesh Karunaratne) + - Debugging: . Fixed bug GH-15923 (GDB: Python Exception : exceptions must derive from BaseException). (nielsdos) diff --git a/UPGRADING b/UPGRADING index 97740040a0e90..3c40a708ed4fc 100644 --- a/UPGRADING +++ b/UPGRADING @@ -302,6 +302,13 @@ PHP 8.4 UPGRADE NOTES to allow or abort the request. . Added CURLOPT_SERVER_RESPONSE_TIMEOUT, which was formerly known as CURLOPT_FTP_RESPONSE_TIMEOUT. Both constants hold the same value. + . Added CURLOPT_DEBUGFUNCTION support. This Curl option accepts a callable + that gets called during the request lifetime with the CurlHandle object, + an integer containing the debug message type, and a string containing the + debug message. The debug message type is one of CURLINFO_TEXT, CURLINFO_HEADER_IN, + CURLINFO_HEADER_OUT, CURLINFO_DATA_IN, CURLINFO_DATA_OUT, CURLINFO_SSL_DATA_OUT, + CURLINFO_SSL_DATA_IN constants. Once this option is set, CURLINFO_HEADER_OUT + must not be set because it uses the same libcurl functionality. - Date: . Added static methods @@ -1041,6 +1048,13 @@ PHP 8.4 UPGRADE NOTES . CURL_PREREQFUNC_OK. . CURL_PREREQFUNC_ABORT. . CURLOPT_SERVER_RESPONSE_TIMEOUT. + . CURLOPT_DEBUGFUNCTION. + . CURLINFO_TEXT. + . CURLINFO_HEADER_IN. + . CURLINFO_DATA_IN. + . CURLINFO_DATA_OUT. + . CURLINFO_SSL_DATA_OUT. + . CURLINFO_SSL_DATA_IN. - Intl: . The IntlDateFormatter class exposes now the new PATTERN constant diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index e6245aab073e2..3cd442674573b 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -486,6 +486,48 @@ */ const CURLOPT_XFERINFOFUNCTION = UNKNOWN; +/** + * @var int + * @cvalue CURLOPT_DEBUGFUNCTION + */ +const CURLOPT_DEBUGFUNCTION = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_TEXT + */ +const CURLINFO_TEXT = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_HEADER_IN + */ +const CURLINFO_HEADER_IN = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_DATA_IN + */ +const CURLINFO_DATA_IN = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_DATA_OUT + */ +const CURLINFO_DATA_OUT = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_SSL_DATA_OUT + */ +const CURLINFO_SSL_DATA_OUT = UNKNOWN; + +/** + * @var int + * @cvalue CURLINFO_SSL_DATA_IN + */ +const CURLINFO_SSL_DATA_IN = UNKNOWN; + /* */ /** * @var int diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index 99db69c3f7856..b472bafe8737a 100644 --- a/ext/curl/curl_arginfo.h +++ b/ext/curl/curl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 5aa5f230880f8373ef8ec378f7e600247332136e */ + * Stub hash: c5e16a7da3f25d061813235a262501e0d8747ccb */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) @@ -312,6 +312,13 @@ static void register_curl_symbols(int module_number) REGISTER_LONG_CONSTANT("CURLOPT_WRITEFUNCTION", CURLOPT_WRITEFUNCTION, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_WRITEHEADER", CURLOPT_WRITEHEADER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_XFERINFOFUNCTION", CURLOPT_XFERINFOFUNCTION, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLOPT_DEBUGFUNCTION", CURLOPT_DEBUGFUNCTION, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_TEXT", CURLINFO_TEXT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_HEADER_IN", CURLINFO_HEADER_IN, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_DATA_IN", CURLINFO_DATA_IN, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_DATA_OUT", CURLINFO_DATA_OUT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_SSL_DATA_OUT", CURLINFO_SSL_DATA_OUT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLINFO_SSL_DATA_IN", CURLINFO_SSL_DATA_IN, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLE_ABORTED_BY_CALLBACK", CURLE_ABORTED_BY_CALLBACK, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLE_BAD_CALLING_ORDER", CURLE_BAD_CALLING_ORDER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLE_BAD_CONTENT_ENCODING", CURLE_BAD_CONTENT_ENCODING, CONST_PERSISTENT); diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 011c4874e6140..19e43094574de 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -68,6 +68,7 @@ typedef struct { zend_fcall_info_cache progress; zend_fcall_info_cache xferinfo; zend_fcall_info_cache fnmatch; + zend_fcall_info_cache debug; #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ zend_fcall_info_cache prereq; #endif diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 3006b67dfce6d..540d326169e86 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -504,6 +504,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n) zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.fnmatch); } + if (ZEND_FCC_INITIALIZED(curl->handlers.debug)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.debug); + } + #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(curl->handlers.prereq)) { zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.prereq); @@ -915,18 +919,46 @@ static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx } /* }}} */ -static int curl_debug(CURL *cp, curl_infotype type, char *buf, size_t buf_len, void *ctx) /* {{{ */ +static int curl_debug(CURL *handle, curl_infotype type, char *data, size_t size, void *clientp) /* {{{ */ { - php_curl *ch = (php_curl *)ctx; + php_curl *ch = (php_curl *)clientp; - if (type == CURLINFO_HEADER_OUT) { - if (ch->header.str) { - zend_string_release_ex(ch->header.str, 0); - } - ch->header.str = zend_string_init(buf, buf_len, 0); - } + #if PHP_CURL_DEBUG + fprintf(stderr, "curl_debug() called\n"); + fprintf(stderr, "type = %d, data = %s\n", type, data); + #endif + + // Implicitly store the headers for compatibility with CURLINFO_HEADER_OUT + // used as a Curl option. Previously, setting CURLINFO_HEADER_OUT set curl_debug + // as the CURLOPT_DEBUGFUNCTION and stored the debug data when type is set to + // CURLINFO_HEADER_OUT. For backward compatibility, we now store the headers + // but also call the user-callback function if available. + if (type == CURLINFO_HEADER_OUT) { + if (ch->header.str) { + zend_string_release_ex(ch->header.str, 0); + } + ch->header.str = zend_string_init(data, size, 0); + } + + if (!ZEND_FCC_INITIALIZED(ch->handlers.debug)) { + return 0; + } - return 0; + zval args[3]; + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + ZVAL_LONG(&args[1], type); + ZVAL_STRINGL(&args[2], data, size); + + ch->in_callback = true; + zend_call_known_fcc(&ch->handlers.debug, NULL, /* param_count */ 3, args, /* named_params */ NULL); + ch->in_callback = false; + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[2]); + + return 0; } /* }}} */ @@ -1096,6 +1128,7 @@ void init_curl_handle(php_curl *ch) ch->handlers.progress = empty_fcall_info_cache; ch->handlers.xferinfo = empty_fcall_info_cache; ch->handlers.fnmatch = empty_fcall_info_cache; + ch->handlers.debug = empty_fcall_info_cache; #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ ch->handlers.prereq = empty_fcall_info_cache; #endif @@ -1269,6 +1302,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source) php_curl_copy_fcc_with_option(ch, CURLOPT_PROGRESSDATA, &ch->handlers.progress, &source->handlers.progress); php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo); php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch); + php_curl_copy_fcc_with_option(ch, CURLOPT_DEBUGDATA, &ch->handlers.debug, &source->handlers.debug); #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ php_curl_copy_fcc_with_option(ch, CURLOPT_PREREQDATA, &ch->handlers.prereq, &source->handlers.prereq); #endif @@ -1632,6 +1666,8 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PROGRESS, handlers.progress, curl_progress); HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_XFERINFO, handlers.xferinfo, curl_xferinfo); HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch); + HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_DEBUG, handlers.debug, curl_debug); + #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PREREQ, handlers.prereq, curl_prereqfunction); #endif @@ -2216,6 +2252,11 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue } case CURLINFO_HEADER_OUT: + if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) { + zend_value_error("CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set"); + return FAILURE; + } + if (zend_is_true(zvalue)) { curl_easy_setopt(ch->cp, CURLOPT_DEBUGFUNCTION, curl_debug); curl_easy_setopt(ch->cp, CURLOPT_DEBUGDATA, (void *)ch); @@ -2795,6 +2836,9 @@ static void curl_free_obj(zend_object *object) if (ZEND_FCC_INITIALIZED(ch->handlers.fnmatch)) { zend_fcc_dtor(&ch->handlers.fnmatch); } + if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) { + zend_fcc_dtor(&ch->handlers.debug); + } #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) { zend_fcc_dtor(&ch->handlers.prereq); @@ -2878,6 +2922,10 @@ static void _php_curl_reset_handlers(php_curl *ch) if (ZEND_FCC_INITIALIZED(ch->handlers.fnmatch)) { zend_fcc_dtor(&ch->handlers.fnmatch); } + + if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) { + zend_fcc_dtor(&ch->handlers.debug); + } #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) { zend_fcc_dtor(&ch->handlers.prereq); diff --git a/ext/curl/sync-constants.php b/ext/curl/sync-constants.php index 8f35f7b05fa38..87868bb5b31d9 100755 --- a/ext/curl/sync-constants.php +++ b/ext/curl/sync-constants.php @@ -18,6 +18,7 @@ 'CURLOPT_PROGRESSDATA', 'CURLOPT_XFERINFODATA', 'CURLOPT_PREREQDATA', + 'CURLOPT_DEBUGDATA', ]; const IGNORED_PHP_CONSTANTS = [ diff --git a/ext/curl/tests/bug48203_multi.phpt b/ext/curl/tests/bug48203_multi.phpt index ed58f8540d9ef..ec9748517ab85 100644 --- a/ext/curl/tests/bug48203_multi.phpt +++ b/ext/curl/tests/bug48203_multi.phpt @@ -4,8 +4,9 @@ Variation of bug #48203 with curl_multi_exec (Crash when file pointers passed to curl --SKIPIF-- --FILE-- diff --git a/ext/curl/tests/bug71523.phpt b/ext/curl/tests/bug71523.phpt index fa32ecdccd596..2cf2477409dae 100644 --- a/ext/curl/tests/bug71523.phpt +++ b/ext/curl/tests/bug71523.phpt @@ -4,8 +4,9 @@ Bug #71523 (Copied handle with new option CURLOPT_HTTPHEADER crashes while curl_ curl --SKIPIF-- --FILE-- diff --git a/ext/curl/tests/curl_basic_018.phpt b/ext/curl/tests/curl_basic_018.phpt index 72778c82e9ad2..1c907a05fb092 100644 --- a/ext/curl/tests/curl_basic_018.phpt +++ b/ext/curl/tests/curl_basic_018.phpt @@ -6,8 +6,9 @@ TestFest 2009 - AFUP - Thomas Rabaix curl --SKIPIF-- --FILE-- diff --git a/ext/curl/tests/curl_multi_getcontent_basic3.phpt b/ext/curl/tests/curl_multi_getcontent_basic3.phpt index 558440bfe20e5..79002a56430d2 100644 --- a/ext/curl/tests/curl_multi_getcontent_basic3.phpt +++ b/ext/curl/tests/curl_multi_getcontent_basic3.phpt @@ -7,8 +7,9 @@ Rein Velt (rein@velt.org) curl --SKIPIF-- --FILE-- diff --git a/ext/curl/tests/curl_setopt_CURLOPT_DEBUGFUNCTION.phpt b/ext/curl/tests/curl_setopt_CURLOPT_DEBUGFUNCTION.phpt new file mode 100644 index 0000000000000..7193642dcab04 --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_DEBUGFUNCTION.phpt @@ -0,0 +1,236 @@ +--TEST-- +Curl option CURLOPT_DEBUGFUNCTION +--EXTENSIONS-- +curl +--FILE-- +getMessage()); +} +$chCopy = curl_copy_handle($ch); +try { + var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true)); +} +catch (\ValueError $e) { + var_dump($e->getMessage()); +} +var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null)); +var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true)); + +echo "\n===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw===\n"; +$ch = curl_init(); +var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true)); +var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null)); +var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true)); +var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen')); +$chCopy = curl_copy_handle($ch); +var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen')); +var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null)); +var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, 1)); + +echo "\n===Test CURLOPT_DEBUGFUNCTION=null works===\n"; +$ch = curl_init("{$host}/get.inc?test=file"); +var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null)); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); +var_dump(curl_exec($ch)); + +echo "\n===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect===\n"; +$ch = curl_init("{$host}/get.inc?test=file"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); +var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT)); +var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true)); +var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT)); +var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() {})); +var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT)); +var_dump($result = curl_exec($ch)); +var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT)); + +echo "\n===Test CURLOPT_DEBUGFUNCTION can throw within callback===\n"; +$ch = curl_init("{$host}/get.inc?test=file"); +curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() { + throw new \RuntimeException('This should get caught after verbose=true'); +}); +var_dump(curl_exec($ch)); +curl_setopt($ch, CURLOPT_VERBOSE, true); +try { + var_dump($result = curl_exec($ch)); +} +catch (\RuntimeException $e) { + var_dump($e->getMessage()); +} +var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT)); + +echo "\n===Test CURLOPT_DEBUGFUNCTION on shared handles work===\n"; +$ch = curl_init("{$host}/get.inc?test=file"); +$called = false; +curl_setopt_array($ch, [ + CURLOPT_VERBOSE => true, + CURLOPT_DEBUGFUNCTION => static function() use (&$called) { + $called = true; + }, +]); +var_dump($called); +curl_exec($ch); +var_dump($called); +$called = false; +$ch2 = curl_copy_handle($ch); +curl_exec($ch2); +var_dump($called); +var_dump(curl_getinfo($ch2, CURLINFO_HEADER_OUT)); + +echo "\nDone"; +?> +--EXPECTF-- +int(20094) + +===Testing with regular CURLOPT_VERBOSE with verbose=false=== +bool(true) + +===Testing with regular CURLOPT_VERBOSE on stderr=== +bool(true) + +===Testing with CURLOPT_DEBUGFUNCTION happy path=== +bool(true) +Received stderr empty: +string(0) "" +string(0) "" +bool(true) + +===Test attempting to set CURLINFO_HEADER_OUT while CURLOPT_DEBUGFUNCTION in effect throws=== +bool(true) +bool(true) +bool(true) +bool(true) +string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set" +string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set" +bool(true) +bool(true) + +===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw=== +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) + +===Test CURLOPT_DEBUGFUNCTION=null works=== +bool(true) +string(0) "" + +===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect=== +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +string(0) "" +string(%d) "GET /get.inc?test=file HTTP/%s +Host: %s:%d +Accept: */* + +" + +===Test CURLOPT_DEBUGFUNCTION can throw within callback=== +bool(true) +string(41) "This should get caught after verbose=true" +string(%d) "GET /get.inc?test=file HTTP/%s +Host: %s:%d +Accept: */* + +" + +===Test CURLOPT_DEBUGFUNCTION on shared handles work=== +bool(false) +bool(true) +bool(true) +string(71) "GET /get.inc?test=file HTTP/%s +Host: %s:%d +Accept: */* + +" + +Done diff --git a/ext/dom/attr.c b/ext/dom/attr.c index 9ad28232f4ed3..dfe3abc1a885c 100644 --- a/ext/dom/attr.c +++ b/ext/dom/attr.c @@ -177,7 +177,8 @@ Since: DOM Level 3 */ zend_result dom_attr_schema_type_info_read(dom_object *obj, zval *retval) { - /* TODO */ + /* This was never implemented, and is dropped from the modern-day DOM spec. + * It only exists in old DOM to preserve BC. */ ZVAL_NULL(retval); return SUCCESS; } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 4b31f2e07ae71..f8a24c18c242a 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -13740,7 +13740,9 @@ static int zend_jit_load_this(zend_jit_ctx *jit, uint32_t var) static int zend_jit_fetch_this(zend_jit_ctx *jit, const zend_op *opline, const zend_op_array *op_array, bool check_only) { - if (!op_array->scope || (op_array->fn_flags & ZEND_ACC_STATIC)) { + if (!op_array->scope || + (op_array->fn_flags & ZEND_ACC_STATIC) || + ((op_array->fn_flags & (ZEND_ACC_CLOSURE|ZEND_ACC_IMMUTABLE)) == ZEND_ACC_CLOSURE)) { if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE) { if (!JIT_G(current_frame) || !TRACE_FRAME_IS_THIS_CHECKED(JIT_G(current_frame))) { diff --git a/ext/opcache/tests/jit/gh15973.phpt b/ext/opcache/tests/jit/gh15973.phpt new file mode 100644 index 0000000000000..42a039cd51c4e --- /dev/null +++ b/ext/opcache/tests/jit/gh15973.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-15973 (Segmentation fault in JIT mode 1135) +--EXTENSIONS-- +opcache +--INI-- +opcache.jit=1215 +opcache.jit_buffer_size=64M +--FILE-- +prop[] = 1; +})->bindTo($test, Test::class); +$appendProp2(); +?> +--EXPECTF-- +Warning: Undefined variable $test in %sgh15973.php on line 6 + +Fatal error: Uncaught Error: Using $this when not in object context in %sgh15973.php:5 +Stack trace: +#0 %sgh15973.php(7): Test::{closure:%s}() +#1 {main} + thrown in %sgh15973.php on line 5