diff --git a/CHANGELOG b/CHANGELOG index af3ab792bf5..b0b93226c21 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ * Speed up applying `html_cva()` recipes that resolve to classes already seen * Fix a class marked safe for `html_attr` not being considered safe in `html` and `html_attr_relaxed` contexts * Add a way to mark a `Twig\Markup` instance as safe for some escaping strategies only + * Speed up the `html` escaping strategy for long strings # 3.30.0 (2026-09-25) diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index a2b0df06d37..410a38f0b50 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -29,6 +29,8 @@ final class EscaperRuntime implements RuntimeExtensionInterface 'html_attr_relaxed' => ['html'], ]; + private const HTML_SPECIAL_CHARS = ['&' => '&', '"' => '"', '\'' => ''', '<' => '<', '>' => '>']; + /** @var array */ private $escapers = []; @@ -157,6 +159,11 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu // see https://www.php.net/htmlspecialchars if ('UTF-8' === $charset) { + // Up to PHP 8.6, htmlspecialchars() decodes every character (see https://github.com/php/php-src/pull/23957): on long valid UTF-8 strings, replacing these characters with strtr() is faster + if (\PHP_VERSION_ID < 80700 && \strlen($string) > 32 && preg_match('//u', $string)) { + return strtr($string, self::HTML_SPECIAL_CHARS); + } + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); } diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php index db84d9b7652..c847a3de0c6 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -174,6 +174,25 @@ public function testHtmlEscapingConvertsSpecialChars(): void } } + /** + * @dataProvider provideLongStrings + */ + #[DataProvider('provideLongStrings')] + public function testHtmlEscapingLongStringsLikeHtmlspecialchars(string $string): void + { + $this->assertSame(htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'), (new EscaperRuntime())->escape($string, 'html')); + } + + public static function provideLongStrings(): iterable + { + $padding = str_repeat('padding ', 5); + + yield 'nothing to escape' => [$padding]; + yield 'special chars' => [$padding.'Tom & Jerry\'s']; + yield 'multibyte chars' => [$padding.'éàü 😀']; + yield 'invalid UTF-8' => [$padding."\xC3\x28 \xFF \xED\xA0\x80 &"]; + } + public function testHtmlAttributeEscapingConvertsSpecialChars(): void { foreach ($this->htmlAttrSpecialChars as $key => $value) {