One MUST specify ENT_HTML5 in addition to double_encode=false to avoid double-encoding.
The reason is that contrary to the documentation, double_encode=false will NOT unconditionally and globally prevent double-encoding of ALL existing entities. Crucially, it will only skip double-encoding for THOSE character entities that are explicitly valid for the document type chosen!
Since ENT_HTML5 references the most expansive list of character entities, it is the only setting that will be most lenient with existing character entities.
<?php
declare(strict_types=1);
$text = 'ampersand(&), double quote("), single quote('), less than(<), greater than(>), numeric entities(&"'<>), HTML 5 entities(+,!$(ņ€)';
$result3 = htmlspecialchars( $text, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8', false );
$result4 = htmlspecialchars( $text, ENT_NOQUOTES | ENT_XML1 | ENT_SUBSTITUTE, 'UTF-8', false );
$result5 = htmlspecialchars( $text, ENT_NOQUOTES | ENT_XHTML | ENT_SUBSTITUTE, 'UTF-8', false );
$result6 = htmlspecialchars( $text, ENT_NOQUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8', false );
echo "<br />\r\nHTML 4.01:<br />\r\n", $result3,
"<br />\r\nXML 1:<br />\r\n", $result4,
"<br />\r\nXHTML:<br />\r\n", $result5,
"<br />\r\nHTML 5:<br />\r\n", $result6, "<br />\r\n";
?>
will produce:
HTML 4.01 (will NOT recognize single quote, but Euro):
ampersand(&), double quote("), single quote('), less than(<), greater than(>), numeric entities(&"'<>), HTML 5 entities(+,!$(ņ€)
XML 1 (WILL recognize single quote, but NOT Euro):
ampersand(&), double quote("), single quote('), less than(<), greater than(>), numeric entities(&"'<>), HTML 5 entities(+,!$(ņ€)
XHTML (recognizes single quote and Euro):
ampersand(&), double quote("), single quote('), less than(<), greater than(>), numeric entities(&"'<>), HTML 5 entities(+,!$(ņ€)
HTML 5 (recognizes "all" valid character entities):
ampersand(&), double quote("), single quote('), less than(<), greater than(>), numeric entities(&"'<>), HTML 5 entities(+,!$(?€)