{"record":{"id":"0ace3a7d1dea9510","repo":"PHPOffice/PhpSpreadsheet","slug":"merge-must-be-on-a-valid-range-of-cells","errorCode":null,"errorMessage":"Merge must be on a valid range of cells.","messagePattern":"Merge must be on a valid range of cells\\.","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Worksheet/Worksheet.php","lineNumber":1851,"sourceCode":"     *              or an AddressRange.\n     * @param string $behaviour How the merged cells should behave.\n     *               Possible values are:\n     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells\n     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells\n     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell\n     *\n     * @return $this\n     */\n    public function mergeCells(AddressRange|string|array $range, string $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static\n    {\n        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));\n\n        if (!str_contains($range, ':')) {\n            $range .= \":{$range}\";\n        }\n\n        if (!Preg::isMatch('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches)) {\n            throw new Exception('Merge must be on a valid range of cells.');\n        }\n\n        $this->mergeCells[$range] = $range;\n        $firstRow = (int) $matches[2];\n        $lastRow = (int) $matches[4];\n        $firstColumn = $matches[1];\n        $lastColumn = $matches[3];\n        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);\n        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);\n        $numberRows = $lastRow - $firstRow;\n        $numberColumns = $lastColumnIndex - $firstColumnIndex;\n\n        if ($numberRows === 1 && $numberColumns === 1) {\n            return $this;\n        }\n\n        // create upper left cell if it does not already exist\n        $upperLeft = \"{$firstColumn}{$firstRow}\";","sourceCodeStart":1833,"sourceCodeEnd":1869,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Worksheet/Worksheet.php#L1833-L1869","documentation":"Worksheet::mergeCells() normalizes its input (sheet-name prefix trimmed, single cell expanded to 'X:Y') and then requires the result to match ^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$ — uppercase letters, then digits, on both sides. Anything else (lowercase column letters, non-coordinate text, malformed addresses) fails the regex and throws.","triggerScenarios":"$sheet->mergeCells('a1:b2') — lowercase letters fail the regex; mergeCells('total'); mergeCells('A-1:B2'); values concatenated with stray characters ('A1 :B2'). The array/CellRange forms are normalized by Validations, so this mainly bites string input.","commonSituations":"Passing user-typed or external-system range strings directly (often lowercase); ranges assembled from unchecked variables where a piece is empty or non-numeric.","solutions":["Normalize strings before merging: $range = strtoupper(trim($range)); then mergeCells($range)","Build the range from trusted parts: Coordinate::stringFromColumnIndex($c) . $row . ':' . ...","Prefer the structured forms — mergeCells([fromCol, fromRow, toCol, toRow]) or a CellRange — which are validated/normalized for you"],"exampleFix":"// before\n$sheet->mergeCells('c5:f8'); // lowercase -> regex fails\n\n// after\n$sheet->mergeCells(strtoupper('c5:f8')); // 'C5:F8'\n// or structured:\n$sheet->mergeCells([3, 5, 6, 8]);","handlingStrategy":"validation","validationCode":"$range = strtoupper(trim($range));\nif (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range) === 1) {\n    $sheet->mergeCells($range);\n} else {\n    throw new InvalidArgumentException(\"Invalid merge range '$range'\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Uppercase all incoming range strings — the merge regex is case-sensitive","Build ranges from numeric parts via Coordinate::stringFromColumnIndex() instead of string surgery","Prefer array [fromCol, fromRow, toCol, toRow] or CellRange inputs for dynamic merges"],"tags":["phpspreadsheet","merge-cells","range-format","regex-validation","uppercase"],"backgroundTag":"invalid-range-format","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}