Table of Contents

PHP RFC: Allow throwing exceptions from __toString()

Introduction

Throwing exceptions from __toString() is currently forbidden and will result in a fatal error. This makes it dangerous to call arbitrary code inside __toString() and makes its use as a general API problematic. This RFC aims to remove this restriction.

The rationale for the current behavior is that string conversions are performed in many places throughout the engine and standard library, and not all places are prepared to deal with exceptions “correctly”, in the sense that the exception is handled as early as possible.

This limitation is ultimately futile from a technical perspective, because exceptions during string conversion can still be triggered by an error handler that converts recoverable errors to exceptions:

set_error_handler(function() {
    throw new Exception();
});
 
try {
    (string) new stdClass;
} catch (Exception $e) {
    echo "(string) threw an exception...\n";
}

In fact, Symfony uses this loophole to work around the current limitation. Unfortunately this relies on the $errcontext parameter, which is going away in PHP 8.

Despite this, the sentiment in past discussions on this topic has been to not relax this restriction until we have performed a comprehensive audit of string conversions across the codebase. This has been done in the attached implementation pull request.

Proposal

Allow throwing exceptions from __toString(), which will behave as usual. Do not trigger a fatal error anymore.

Additionally convert the “could not be converted to string” and “__toString() must return a string value” recoverable fatal errors into proper Error exceptions, in line with the error policy established in PHP 7.

Extension Guidelines

Extension authors who would like to ensure that they handle exceptions from string conversions gracefully, should take the following guidelines into account:

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // Possibly free other resources here.
    return;
}
// Like zval_get_string() but returns NULL on conversion failure.
zend_string *str = zval_try_get_string(val);
if (!str) {
    // Possibly free other resources here.
    return;
}
// Main code.
zend_string_release(str);
 
 
// Like zval_get_tmp_string() but returns NULL on conversion failure.
zend_string *tmp, *str = zval_try_get_tmp_string(val, &tmp);
if (!str) {
    // Possibly free other resources here.
    return;
}
// Main code.
zend_tmp_string_release(tmp);
 
 
// Like convert_to_string() but returns a boolean indicating conversion success/failure.
if (!try_convert_to_string(val)) {
    // Possibly free other resources here.
    return;
}
// Main code.

Backward Incompatible Changes

The conversion from recoverable fatal errors to Error exceptions is technically BC breaking.

Vote

Voting started 2019-05-22 and ends 2019-06-05.

Allow exceptions from __toString() in PHP 7.4?
Real name Yes No
ab (ab)  
ajf (ajf)  
ashnazg (ashnazg)  
bishop (bishop)  
brzuchal (brzuchal)  
bwoebi (bwoebi)  
carusogabriel (carusogabriel)  
cmb (cmb)  
danack (danack)  
derick (derick)  
didou (didou)  
duncan3dc (duncan3dc)  
galvao (galvao)  
gasolwu (gasolwu)  
girgias (girgias)  
jasny (jasny)  
jhdxr (jhdxr)  
kalle (kalle)  
kelunik (kelunik)  
kguest (kguest)  
levim (levim)  
marcio (marcio)  
mariano (mariano)  
mbeccati (mbeccati)  
mightyuhu (mightyuhu)  
mike (mike)  
nikic (nikic)  
ocramius (ocramius)  
ramsey (ramsey)  
rasmus (rasmus)  
reywob (reywob)  
rtheunissen (rtheunissen)  
salathe (salathe)  
sammyk (sammyk)  
santiagolizardo (santiagolizardo)  
sebastian (sebastian)  
sergey (sergey)  
stas (stas)  
subjective (subjective)  
svpernova09 (svpernova09)  
trowski (trowski)  
yunosh (yunosh)  
Final result: 42 0
This poll has been closed.