Deprecate returning non-string values from a user output handler (#18932)

https://wiki.php.net/rfc/deprecations_php_8_4
This commit is contained in:
DanielEScherzer 2025-07-07 14:31:13 -07:00 committed by GitHub
parent 6cc21c4ee6
commit d8577d9bfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 603 additions and 17 deletions

View file

@ -259,6 +259,13 @@ PHP 8.5 UPGRADE NOTES
4. Deprecated Functionality
========================================
- Core:
. Returning a non-string from a user output handler is deprecated. The
deprecation warning will bypass the handler with the bad return to ensure
it is visible; if there are nested output handlers the next one will still
be used.
RFC: https://wiki.php.net/rfc/deprecations_php_8_4
- Hash:
. The MHASH_* constants have been deprecated. These have been overlooked
when the mhash*() function family has been deprecated per

View file

@ -8,6 +8,7 @@ $counter = 0;
ob_start(function ($buffer) use (&$c, &$counter) {
$c = 0;
++$counter;
return '';
}, 1);
$c .= [];
$c .= [];

View file

@ -7,6 +7,7 @@ opcache.optimization_level = 0x7FFEBFFF & ~0x400
$x = 'non-empty';
ob_start(function () use (&$c) {
$c = 0;
return '';
}, 1);
$c = [];
$x = $c . $x;

View file

@ -6,6 +6,7 @@ $c = str_repeat("abcd", 10);
ob_start(function () use (&$c) {
$c = 0;
return '';
}, 1);
class X {

View file

@ -11,6 +11,7 @@ ob_start(function() {
register_tick_function(
function() { }
);
return '';
});
?>
--EXPECT--

View file

@ -18,6 +18,7 @@ ob_start(function() {
$a[] = 2;
}
fwrite(STDOUT, "Success");
return '';
});
$a = [];

View file

@ -18,6 +18,7 @@ ob_start(function() {
$a[] = 2;
}
fwrite(STDOUT, "Success");
return '';
});
$a = ["not packed" => 1];

View file

@ -6,6 +6,7 @@ $counter = 0;
ob_start(function ($buffer) use (&$c, &$counter) {
$c = 0;
++$counter;
return '';
}, 1);
$c .= [];
$c .= [];

View file

@ -5,7 +5,7 @@ session
--FILE--
<?php
function output_html($ext) {
return strlen($ext);
return (string)strlen($ext);
}
class MySessionHandler implements SessionHandlerInterface {

View file

@ -934,6 +934,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
return PHP_OUTPUT_HANDLER_FAILURE;
}
bool still_have_handler = true;
/* storable? */
if (php_output_handler_append(handler, &context->in) && !context->op) {
context->op = original_op;
@ -948,6 +949,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
if (handler->flags & PHP_OUTPUT_HANDLER_USER) {
zval ob_args[2];
zval retval;
ZVAL_UNDEF(&retval);
/* ob_data */
ZVAL_STRINGL(&ob_args[0], handler->buffer.data, handler->buffer.used);
@ -959,17 +961,48 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
handler->func.user->fci.params = ob_args;
handler->func.user->fci.retval = &retval;
#define PHP_OUTPUT_USER_SUCCESS(retval) ((Z_TYPE(retval) != IS_UNDEF) && !(Z_TYPE(retval) == IS_FALSE))
if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && PHP_OUTPUT_USER_SUCCESS(retval)) {
/* user handler may have returned TRUE */
status = PHP_OUTPUT_HANDLER_NO_DATA;
if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) {
convert_to_string(&retval);
if (Z_STRLEN(retval)) {
context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval));
context->out.used = Z_STRLEN(retval);
context->out.free = 1;
status = PHP_OUTPUT_HANDLER_SUCCESS;
if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) {
if (Z_TYPE(retval) != IS_STRING) {
// Make sure that we don't get lost in the current output buffer
// by disabling it
handler->flags |= PHP_OUTPUT_HANDLER_DISABLED;
php_error_docref(
NULL,
E_DEPRECATED,
"Returning a non-string result from user output handler %s is deprecated",
ZSTR_VAL(handler->name)
);
// Check if the handler is still in the list of handlers to
// determine if the PHP_OUTPUT_HANDLER_DISABLED flag can
// be removed
still_have_handler = false;
int handler_count = php_output_get_level();
if (handler_count) {
php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers));
for (int handler_num = 0; handler_num < handler_count; ++handler_num) {
php_output_handler *curr_handler = handlers[handler_num];
if (curr_handler == handler) {
handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED);
still_have_handler = true;
break;
}
}
}
}
if (Z_TYPE(retval) == IS_FALSE) {
/* call failed, pass internal buffer along */
status = PHP_OUTPUT_HANDLER_FAILURE;
} else {
/* user handler may have returned TRUE */
status = PHP_OUTPUT_HANDLER_NO_DATA;
if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) {
convert_to_string(&retval);
if (Z_STRLEN(retval)) {
context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval));
context->out.used = Z_STRLEN(retval);
context->out.free = 1;
status = PHP_OUTPUT_HANDLER_SUCCESS;
}
}
}
} else {
@ -996,10 +1029,17 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
status = PHP_OUTPUT_HANDLER_FAILURE;
}
}
handler->flags |= PHP_OUTPUT_HANDLER_STARTED;
if (still_have_handler) {
handler->flags |= PHP_OUTPUT_HANDLER_STARTED;
}
OG(running) = NULL;
}
if (!still_have_handler) {
// Handler and context will have both already been freed
return status;
}
switch (status) {
case PHP_OUTPUT_HANDLER_FAILURE:
/* disable this handler */
@ -1225,6 +1265,19 @@ static int php_output_stack_pop(int flags)
}
php_output_handler_op(orphan, &context);
}
// If it isn't still in the stack, cannot free it
bool still_have_handler = false;
int handler_count = php_output_get_level();
if (handler_count) {
php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers));
for (int handler_num = 0; handler_num < handler_count; ++handler_num) {
php_output_handler *curr_handler = handlers[handler_num];
if (curr_handler == orphan) {
still_have_handler = true;
break;
}
}
}
/* pop it off the stack */
zend_stack_del_top(&OG(handlers));
@ -1240,7 +1293,9 @@ static int php_output_stack_pop(int flags)
}
/* destroy the handler (after write!) */
php_output_handler_free(&orphan);
if (still_have_handler) {
php_output_handler_free(&orphan);
}
php_output_context_dtor(&context);
return 1;

View file

@ -18,6 +18,7 @@ $stderr = fopen('php://stderr', 'r');
ob_start(function ($buffer) use ($stdout) {
fwrite($stdout, $buffer);
return '';
}, 1);
print "STDIN:\n";

View file

@ -34,6 +34,7 @@ file_put_contents('php://fd/2', "Goes to stderrFile\n");
ob_start(function ($buffer) use ($stdoutStream) {
fwrite($stdoutStream, $buffer);
return '';
}, 1);
print "stdoutFile:\n";

View file

@ -5,7 +5,7 @@ Bug #60768 Output buffer not discarded
global $storage;
ob_start(function($buffer) use (&$storage) { $storage .= $buffer; }, 20);
ob_start(function($buffer) use (&$storage) { $storage .= $buffer; return ''; }, 20);
echo str_repeat("0", 20); // fill in the buffer

View file

@ -35,19 +35,24 @@ foreach ($callbacks as $callback) {
}
?>
--EXPECT--
--EXPECTF--
--> Use callback 'return_empty_string':
--> Use callback 'return_false':
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d
My output.
--> Use callback 'return_null':
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d
--> Use callback 'return_string':
I stole your output.
--> Use callback 'return_zero':
0
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d
0

View file

@ -0,0 +1,147 @@
--TEST--
ob_start(): Check behaviour with deprecation converted to exception
--FILE--
<?php
class NotStringable {
public function __construct(public string $val) {}
}
class IsStringable {
public function __construct(public string $val) {}
public function __toString() {
return __CLASS__ . ": " . $this->val;
}
}
$log = [];
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
function return_null($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return null;
}
function return_false($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return false;
}
function return_true($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return true;
}
function return_zero($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return 0;
}
function return_non_stringable($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return new NotStringable($string);
}
function return_stringable($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return new IsStringable($string);
}
$cases = [
'return_null',
'return_false',
'return_true',
'return_zero',
'return_non_stringable',
'return_stringable',
];
foreach ($cases as $case) {
$log = [];
echo "\n\nTesting: $case\n";
ob_start($case);
echo "Inside of $case\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e . "\n";
}
echo "\nEnd of $case, log was:\n";
echo implode("\n", $log);
}
?>
--EXPECTF--
Testing: return_null
ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_null, log was:
return_null: <<<Inside of return_null
>>>
Testing: return_false
Inside of return_false
ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_false, log was:
return_false: <<<Inside of return_false
>>>
Testing: return_true
ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_true, log was:
return_true: <<<Inside of return_true
>>>
Testing: return_zero
0ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_zero, log was:
return_zero: <<<Inside of return_zero
>>>
Testing: return_non_stringable
ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_non_stringable is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, 69)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_non_stringable, log was:
return_non_stringable: <<<Inside of return_non_stringable
>>>
Testing: return_stringable
ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_stringable is deprecated in %s:%d
Stack trace:
#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, 69)
#1 %s(%d): ob_end_flush()
#2 {main}
End of return_stringable, log was:
return_stringable: <<<Inside of return_stringable
>>>

View file

@ -0,0 +1,143 @@
--TEST--
ob_start(): Check behaviour with deprecation converted to exception
--FILE--
<?php
class NotStringable {
public function __construct(public string $val) {}
}
class IsStringable {
public function __construct(public string $val) {}
public function __toString() {
return __CLASS__ . ": " . $this->val;
}
}
$log = [];
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
function return_null($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return null;
}
function return_false($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return false;
}
function return_true($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return true;
}
function return_zero($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return 0;
}
function return_non_stringable($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return new NotStringable($string);
}
function return_stringable($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return new IsStringable($string);
}
ob_start('return_null');
ob_start('return_false');
ob_start('return_true');
ob_start('return_zero');
ob_start('return_non_stringable');
ob_start('return_stringable');
echo "In all of them\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_stringable handler\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_non_stringable handler\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_zero handler\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_true handler\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_false handler\n\n";
try {
ob_end_flush();
} catch (\ErrorException $e) {
echo $e->getMessage() . "\n";
}
echo "Ended return_null handler\n\n";
echo "All handlers are over\n\n";
echo implode("\n", $log);
?>
--EXPECT--
ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated
Ended return_null handler
All handlers are over
return_stringable: <<<In all of them
>>>
return_non_stringable: <<<ob_end_flush(): Returning a non-string result from user output handler return_stringable is deprecated
Ended return_stringable handler
>>>
return_zero: <<<ob_end_flush(): Returning a non-string result from user output handler return_non_stringable is deprecated
Ended return_non_stringable handler
>>>
return_true: <<<0ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated
Ended return_zero handler
>>>
return_false: <<<ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated
Ended return_true handler
>>>
return_null: <<<ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated
Ended return_true handler
ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated
Ended return_false handler
>>>

View file

@ -0,0 +1,23 @@
--TEST--
ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns false)
--INI--
memory_limit=2M
--FILE--
<?php
ob_start(function() {
// We are out of memory, now trigger a deprecation
return false;
});
$a = [];
// trigger OOM in a resize operation
while (1) {
$a[] = 1;
}
?>
--EXPECTF--
Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

View file

@ -0,0 +1,30 @@
--TEST--
ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns stringable object)
--INI--
memory_limit=2M
--FILE--
<?php
class IsStringable {
public function __construct(public string $val) {}
public function __toString() {
return __CLASS__ . ": " . $this->val;
}
}
ob_start(function() {
// We are out of memory, now trigger a deprecation
return new IsStringable("");
});
$a = [];
// trigger OOM in a resize operation
while (1) {
$a[] = 1;
}
?>
--EXPECTF--
Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

View file

@ -0,0 +1,32 @@
--TEST--
ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns non-stringable object)
--INI--
memory_limit=2M
--FILE--
<?php
class NotStringable {
public function __construct(public string $val) {}
}
ob_start(function() {
// We are out of memory, now trigger a deprecation
return new NotStringable("");
});
$a = [];
// trigger OOM in a resize operation
while (1) {
$a[] = 1;
}
?>
--EXPECTF--
Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
Fatal error: Uncaught Error: Object of class NotStringable could not be converted to string in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View file

@ -0,0 +1,23 @@
--TEST--
ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns true)
--INI--
memory_limit=2M
--FILE--
<?php
ob_start(function() {
// We are out of memory, now trigger a deprecation
return true;
});
$a = [];
// trigger OOM in a resize operation
while (1) {
$a[] = 1;
}
?>
--EXPECTF--
Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

View file

@ -0,0 +1,23 @@
--TEST--
ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns zero)
--INI--
memory_limit=2M
--FILE--
<?php
ob_start(function() {
// We are out of memory, now trigger a deprecation
return 0;
});
$a = [];
// trigger OOM in a resize operation
while (1) {
$a[] = 1;
}
?>
--EXPECTF--
Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d
Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

View file

@ -0,0 +1,89 @@
--TEST--
ob_start(): Check behaviour with multiple nested handlers with had return values
--FILE--
<?php
$log = [];
function return_given_string($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return $string;
}
function return_empty_string($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return "";
}
function return_false($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return false;
}
function return_true($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return true;
}
function return_null($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return null;
}
function return_string($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return "I stole your output.";
}
function return_zero($string) {
global $log;
$log[] = __FUNCTION__ . ": <<<" . $string . ">>>";
return 0;
}
ob_start('return_given_string');
ob_start('return_empty_string');
ob_start('return_false');
ob_start('return_true');
ob_start('return_null');
ob_start('return_string');
ob_start('return_zero');
echo "Testing...";
ob_end_flush();
ob_end_flush();
ob_end_flush();
ob_end_flush();
ob_end_flush();
ob_end_flush();
ob_end_flush();
echo "\n\nLog:\n";
echo implode("\n", $log);
?>
--EXPECTF--
Log:
return_zero: <<<Testing...>>>
return_string: <<<
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d
0>>>
return_null: <<<I stole your output.>>>
return_true: <<<
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d
>>>
return_false: <<<
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d
>>>
return_empty_string: <<<
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d
Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d
>>>
return_given_string: <<<>>>