PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Writer\Exception

Unknown token $token

Error message

Unknown token $token

What it means

After the Xls formula parser builds a token tree, convert() maps each node to a BIFF8 ptg token. The final branch handles only the 'arg' marker and true/false literals; any other token that reaches it has no binary serialization path and triggers 'Unknown token X' with the offending token embedded in the message. This is the parser's catch-all for formula constructs the Xls writer has not implemented.

Source

Thrown at src/PhpSpreadsheet/Writer/Xls/Parser.php:574

        }
        // commented so argument number can be processed correctly. See toReversePolish().
        /*if (Preg::isMatch("/[A-Z0-9\xc0-\xdc\.]+/", $token))
        {
            return($this->convertFunction($token, $this->_func_args));
        }*/
        // if it's an argument, ignore the token (the argument remains)
        if ($token == 'arg') {
            return '';
        }
        if (Preg::isMatch('/^true$/i', $token)) {
            return $this->convertBool(1);
        }
        if (Preg::isMatch('/^false$/i', $token)) {
            return $this->convertBool(0);
        }

        // TODO: use real error codes
        throw new WriterException("Unknown token $token");
    }

    /**
     * Convert a number token to ptgInt or ptgNum.
     *
     * @param float|int|string $num an integer or double for conversion to its ptg value
     */
    private function convertNumber(mixed $num): string
    {
        // Integer in the range 0..2**16-1
        if ((Preg::isMatch('/^\d+$/', (string) $num)) && ($num <= 65535)) {
            return pack('Cv', $this->ptg['ptgInt'], $num);
        }

        // A float
        if (BIFFwriter::getByteOrder()) { // if it's Big Endian
            $num = strrev((string) $num);
        }

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Read the token from the message and locate the cell(s) using it (iterate coordinates and compare getValue() against the token).
  2. Rewrite the affected formula using constructs the BIFF8 writer supports.
  3. Save as Xlsx instead - the Xlsx writer serializes the formula string directly and never runs this ptg conversion.
  4. If you cannot change formulas, write computed values only (setPreCalculateFormulas plus replacing '=...' with cached results) before using the Xls writer.

Example fix

// before
$sheet->getCell('A5')->setValue('=SUMPRODUCT((A1:A3>2)*(B1:B3))');
(new \PhpOffice\PhpSpreadsheet\Writer\Xls($spreadsheet))->save('out.xls');
// WriterException: Unknown token ...

// after: Xlsx writer does not convert formulas to ptg tokens
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('out.xlsx');
Defensive patterns

Strategy: try-catch

Try / catch

try {
    (new \PhpOffice\PhpSpreadsheet\Writer\Xls($spreadsheet))->save($path);
} catch (\PhpOffice\PhpSpreadsheet\Writer\Exception $e) {
    if (str_starts_with($e->getMessage(), 'Unknown token')) {
        // message contains the token; scan cells to find and report the offending formula
        $token = trim(substr($e->getMessage(), strlen('Unknown token')));
        foreach ($spreadsheet->getAllSheets() as $s) {
            foreach ($s->getCoordinates() as $coord) {
                $v = $s->getCell($coord)->getValue();
                if (is_string($v) && str_contains($v, $token)) {
                    error_log(sprintf('Xls-incompatible formula in %s!%s: %s', $s->getTitle(), $coord, $v));
                }
            }
        }
    }
    throw $e;
}

Prevention

When it happens

Trigger: A cell formula containing a token that survives parsing but has no ptg conversion - typically exotic operators, identifiers, or constructs added to the Calculation engine but never taught to the BIFF8 serializer; the message names the exact token that failed.

Common situations: Spreadsheets authored in Excel or imported from Xlsx that use newer syntax, then re-saved as Xls by an automated conversion service; formulas that calculate fine in memory (so tests pass) but explode only at save() time.

Related errors


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/2d35a6a37c38d604. Report an issue: GitHub.