PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Calculation\Exception
Unsupported binary comparison operator
Error message
Unsupported binary comparison operator
What it means
XmlScanner::toUtf8() normalizes every XML payload it loads to UTF-8 before libxml parses it. Before converting, it records whether the payload starts with an XML declaration via '/^.{0,4}\s*<?xml/s'; if the declaration was detectable before mb_convert_encoding() but is no longer detectable after it, the conversion has mangled the document start. That only happens when the file's real byte encoding does not match the charset PhpSpreadsheet detected (from the BOM via Csv::guessEncodingBom() or the encoding="..." declaration), i.e. the file is mis-declared or already double-encoded, so the load is aborted with 'Double encoding not permitted'.
Source
Thrown at src/PhpSpreadsheet/Calculation/BinaryComparison.php:89
}
$useLowercaseFirstComparison = is_string($operand1)
&& is_string($operand2)
&& Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE;
return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison);
}
private static function evaluateComparison(mixed $operand1, mixed $operand2, string $operator, bool $useLowercaseFirstComparison): bool
{
return match ($operator) {
'=' => self::equal($operand1, $operand2),
'>' => self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison),
'<' => self::lessThan($operand1, $operand2, $useLowercaseFirstComparison),
'>=' => self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison),
'<=' => self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison),
'<>' => self::notEqual($operand1, $operand2),
default => throw new Exception('Unsupported binary comparison operator'),
};
}
private static function equal(mixed $operand1, mixed $operand2): bool
{
if (is_numeric($operand1) && is_numeric($operand2)) {
$result = (abs($operand1 - $operand2) < self::DELTA);
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
$result = $operand1 == $operand2;
} else {
$result = self::strcmpAllowNull($operand1, $operand2) == 0;
}
return $result;
}
private static function greaterThanOrEqual(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool
{View on GitHub (pinned to 65b080eef4)
Solutions
- Re-export or re-save the file as UTF-8 (with a matching encoding="UTF-8" declaration or no declaration at all) and retry the load.
- Verify the file's true encoding against its declaration: check the BOM bytes with bin2hex(substr($raw,0,4)) and compare with the XML declaration before loading.
- If you produce the XML yourself, drop the XML declaration or make encoding="UTF-8" explicit so findCharSet() returns UTF-8 and no conversion runs.
- Pre-normalize untrusted input before load(): strip the mismatched declaration with preg_replace('/^<\?xml[^>]*\?>/', '', $raw), convert with mb_convert_encoding($raw,'UTF-8',$realCharset), write to a temp file, and load that.
Example fix
// before
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('export.xml');
// throws: Double encoding not permitted (declaration says UTF-16, bytes are UTF-8)
// after - normalize the declaration/encoding mismatch before loading
$raw = file_get_contents('export.xml');
$raw = preg_replace('/^<\?xml[^>]*\?>/', '', $raw) ?? $raw; // drop mismatched declaration
$utf8 = mb_convert_encoding($raw, 'UTF-8', 'UTF-8');
$tmp = tempnam(sys_get_temp_dir(), 'xlsx');
file_put_contents($tmp, $utf8);
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($tmp); Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: does the declared/BOM charset match reality before loading?
function encodingLooksConsistent(string $path): bool
{
$raw = file_get_contents($path);
if ($raw === false || $raw === '') {
return false;
}
$bom = strtoupper(bin2hex(substr($raw, 0, 4)));
$declared = null;
if (preg_match('/^\s*<\?xml[^>]*encoding\s*=\s*(["\'])(.+?)\1/s', $raw, $m)) {
$declared = strtoupper($m[2]);
}
$looksAsciiXml = (bool) preg_match('/^.{0,4}\s*<\?xml/s', $raw);
// ASCII-readable declaration + multi-byte BOM/declaration => will double-convert
if ($looksAsciiXml && ($bom === 'FFFE0000' || $bom === 'FEFF' || $bom === '0000FFFE' || $declared === 'UTF-16')) {
return false;
}
return true;
} Try / catch
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
try {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path);
} catch (ReaderException $e) {
if (str_contains($e->getMessage(), 'Double encoding')) {
// normalize the file to UTF-8 and retry once, or report to the uploader
log::warn('Encoding mismatch in upload', ['file' => $path]);
}
throw $e; // or return an error result
} Prevention
- Generate spreadsheet/XML exports as UTF-8 and declare encoding="UTF-8" (or omit the declaration).
- Never re-encode a file's body while leaving its original XML declaration untouched.
- Validate BOM bytes against the declared encoding at the upload boundary before any reader sees the file.
- When converting files through pipelines (iconv, mb_convert_encoding), rewrite the XML declaration to match the output charset.
- For user uploads, offer a 're-save as UTF-8 from Excel' hint when this exception fires - it resolves the vast majority of cases.
When it happens
Trigger: Loading a file through IOFactory::load() or any reader (Xlsx, Xml, Html, ...) where findCharSet() returns something other than UTF-8 (e.g. a UTF-16 BOM, or encoding="UTF-16"/"ISO-8859-1" in the declaration) while the actual bytes are plain UTF-8/ASCII text. mb_convert_encoding() from the wrong source charset garbles the leading '<?xml' token, the post-conversion regex no longer matches, and Reader\Exception is thrown at XmlScanner.php:48.
Common situations: Spreadsheet/XML exports re-saved by editors (Notepad++, IDEs) or piped through iconv that re-encoded the body but left the original encoding declaration; UTF-16 files whose BOM was stripped or re-added by a transfer layer; concatenating XML fragments produced in different encodings; user-uploaded .xls/.xml from third-party tools that write inconsistent declarations.
Related errors
- Unsupported numeric binary operation
- Cloning the calculation engine is not allowed!
- Locale file not found
- #VALUE!
- #NUM!
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/0146341d361519dc.
Report an issue: GitHub.