diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -580,6 +580,7 @@ 'phutil_safe_html' => 'markup/render.php', 'phutil_set_system_locale' => 'utils/utf8.php', 'phutil_split_lines' => 'utils/utils.php', + 'phutil_string_cast' => 'utils/utils.php', 'phutil_tag' => 'markup/render.php', 'phutil_tag_div' => 'markup/render.php', 'phutil_unescape_uri_path_component' => 'markup/render.php', diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1595,3 +1595,52 @@ 'Unable to decode MIME header: install "iconv" or "mbstring" '. 'extension.')); } + +/** + * Perform a "(string)" cast without disabling standard exception behavior. + * + * When PHP invokes "__toString()" automatically, it fatals if the method + * raises an exception. In older versions of PHP (until PHP 7.1), this fatal is + * fairly opaque and does not give you any information about the exception + * itself, although newer versions of PHP at least include the exception + * message. + * + * This is documented on the "__toString()" manual page: + * + * Warning + * You cannot throw an exception from within a __toString() method. Doing + * so will result in a fatal error. + * + * However, this only applies to implicit invocation by the language runtime. + * Application code can safely call `__toString()` directly without any effect + * on exception handling behavior. Very cool. + * + * We also reject arrays. PHP casts them to the string "Array". This behavior + * is, charitably, evil. + * + * @param wild Any value which aspires to be represented as a string. + * @return string String representation of the provided value. + */ +function phutil_string_cast($value) { + if (is_array($value)) { + throw new Exception( + pht( + 'Value passed to "phutil_string_cast()" is an array; arrays can '. + 'not be sensibly cast to strings.')); + } + + if (is_object($value)) { + $string = $value->__toString(); + + if (!is_string($string)) { + throw new Exception( + pht( + 'Object (of class "%s") did not return a string from "__toString()".', + get_class($value))); + } + + return $string; + } + + return (string)$value; +}