Problem/Motivation
From time to time I'm facing a very straightforward error:
php.ERROR: TypeError: Drupal\Component\Utility\Html::escape(): Argument #1 ($text) must be of type string, null given, called in /var/www/html/web/core/lib/Drupal/Component/Render/FormattableMarkup.php on line 238 in Drupal\Component\Utility\Html::escape() (line 433 of /var/www/html/web/core/lib/Drupal/Component/Utility/Html.php). {"severity_level":3,"exception":"[object] (TypeError(code: 0): Drupal\\Component\\Utility\\Html::escape(): Argument #1 ($text) must be of type string, null given, called in /var/www/html/web/core/lib/Drupal/Component/Render/FormattableMarkup.php on line 238 at /var/www/html/web/core/lib/Drupal/Component/Utility/Html.php:433)"}
This happens because method \Drupal\Component\Utility\Html::escape has strict string type for its argument:
public static function escape(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
This was done in that issue #3441394: Remove deprecated code in utilities (basically Drupal 11.0+).
The problem is that FormattableMarkup, TranslatableMarkup, PluralTranslatableMarkup and TranslationInterface have very weak PHPDoc type-hints in core for the $arguments parameter, which is just @param array $arguments.
It means, a simple (string) new TranslatableMarkup('@name is cool', ['@name' => NULL]) will end up with an error above. The other problem, which is not related to Html::escape, but still, very heavily related, is that the key for $arguments array can't be an empty string, because in \Drupal\Component\Render\FormattableMarkup::placeholderFormat we have:
foreach ($args as $key => $value) { switch ($key[0]) { // Other switched for '%', '!', ':'. default: if (!ctype_alnum($key[0])) { // Warn for random placeholders that won't be replaced. trigger_error(sprintf('Placeholders must begin with one of the following "@", ":" or "%%", invalid placeholder (%s) with string: "%s"', $key, $string), E_USER_WARNING); } // No replacement possible therefore we can discard the argument. unset($args[$key]); break; } }
It means that an empty string is essentially not a valid value for the key either. This is why it should be non-empty-string.
Steps to reproduce
(string) new TranslatableMarkup('@name is cool', ['@name' => NULL])
Proposed resolution
Narrow the $arguments parameter for those classes and the interface to: @param array<non-empty-string, string|\Stringable> $args. This will allows tools such as PHPStan/Psalm detect those issues before it deployed to production.
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3577295
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- main
changes, plain diff MR !14999
- 3577295-add-strict-type
compare
Comments
Comment #4
niklanFor some reason branch wasn't available for some time, thought it broken:
So I've created it manually (
git checkout -b '3577295-add-strict-type' drupal-3577295/main) and pushed the changes, and it ended up in the main branch. Not sure is it problem or not, don't know how to reset its state now.Comment #5
niklanComment #7
smustgrave commentedThis has popped up on a few issues
https://www.drupal.org/project/drupal/issues/3577075
https://www.drupal.org/project/drupal/issues/3501659
This doesn't address those issues but maybe can help future ones. not sure but I'll mark it.
Comment #9
catchLooks like there's still discussion here.
Comment #10
smustgrave commentedNot sure that thread is actionable to hold this one up?
Comment #11
smustgrave commented@catch per #10, correct me if I'm wrong though.
Comment #12
alexpottI'm not convinced by
<non-empty-string, string|\Stringable>. In my mind int, bool, float, NULL are all stringable. And if you look inthat's exactly what happens.
Looking at actual behaviour of the code
So I think we should change this to be
<non-empty-string, string|\Stringable|int|float>I think it is okay to not accept bools because it does not make much sense and non empty strings too but floats and ints do make sense. Let's employ Postel's law here.
Comment #13
mstrelan commentedI'm not understanding what you mean about placeholderEscape. It looks like only MarkupInterface is cast, otherwise it is passing to Html::escape which only takes a string.
I believe this only works due to type coercion, since we don't have strict types everywhere. If I add
declare(strict_types=1);toFormattableMarkupand repeat your test I get this error:Comment #14
alexpott@mstrelan sure I think we should also add the string cast too. We can do that here or in an issue that also adds tests. But as this is only docs and we don't have strict types on I think it is acceptable to document what we actually do accept without error and that current includes in int and float - and I think it makes sense to do that.