From 16bd7b9183f856049931e4d37d3201f0c32c14ca Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Mon, 9 Aug 2021 22:55:33 -0700 Subject: [PATCH] Data Validations Referencing Another Sheet See issues #1432 and #2149. Data validations on an Xlsx worksheet can be specified in two manners - one (henceforth "internal") if a list is specified from the same sheet, and a different one (henceforth "external") if a list is specified from a different sheet. Xlsx worksheet reader formerly processed only the internal format; PR #2150 fixed this so that both would be processed correctly on read. However, Xlsx worksheet writer outputs data validators only in the internal format, and that does not work for external data validations; it appears, however, that internal data validations can be specified in external format. This PR changes Xlsx worksheet writer to use only the external format. Somewhat surprisingly, this must come after most of the other XML tags that constitute a worksheet. It shares this characteristic (and XML tag) with conditional formatting. The new test case DataValidator2Test includes a worksheet which has both internal and external data validation, as well as conditional formatting. There is some additional namespacing work supporting Data Validations that needs to happen on Xlsx reader. Since that is substantially unchanged with this PR, that work will happen in a future namespacing phase, probably phase 2. However, there are some non-namespace-related changes to Xlsx reader in this PR: - Cell DataValidation adds support for a new property sqref, which is initialized through Xlsx reader using a setSqref method. If not initialized at write time, the code will work as it did before the introduction of this property. In particular, before this change, data validation applied to an entire column (as in the sample spreadsheet) would be applied only through the last populated row. In addition, this also allows a user to extend a Data Validation over a range of cells rather than just a single cell; the new method is added to the documentation. - The topLeft property had formerly been used only for worksheets which use "freeze panes". However, as luck would have it, the sample dataset provided to demonstrate the Data Validations problem uses topLeft without freeze panes, slightly affecting the view when the spreadsheet is initially opened; PhpSpreadsheet will now do so as well. It is worth noting issue #2262, which documents a problem with the hasValidValue method involving the calculation engine. That problem existed before this PR, and I do not yet have a handle on how it might be fixed. --- docs/topics/recipes.md | 5 ++ phpstan-baseline.neon | 5 -- src/PhpSpreadsheet/Cell/DataValidation.php | 15 ++++ .../Reader/Xlsx/DataValidations.php | 1 + src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php | 8 ++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 7 ++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 46 ++++++++--- .../Cell/DataValidator2Test.php | 75 ++++++++++++++++++ tests/data/Reader/XLSX/issue.1432b.xlsx | Bin 0 -> 12936 bytes 9 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Cell/DataValidator2Test.php create mode 100644 tests/data/Reader/XLSX/issue.1432b.xlsx diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 62260fc6ef..14c82fe561 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1114,6 +1114,11 @@ ruleset: $spreadsheet->getActiveSheet()->getCell('B8')->setDataValidation(clone $validation); ``` +Alternatively, one can apply the validation to a range of cells: +```php +$validation->setSqref('B5:B1048576'); +``` + ## Setting a column's width A column's width can be set using the following code: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b970b1f34f..3f491e52d3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6315,11 +6315,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" count: 19 diff --git a/src/PhpSpreadsheet/Cell/DataValidation.php b/src/PhpSpreadsheet/Cell/DataValidation.php index dfeb024c92..2ff52f3260 100644 --- a/src/PhpSpreadsheet/Cell/DataValidation.php +++ b/src/PhpSpreadsheet/Cell/DataValidation.php @@ -478,4 +478,19 @@ public function __clone() } } } + + /** @var ?string */ + private $sqref; + + public function getSqref(): ?string + { + return $this->sqref; + } + + public function setSqref(?string $str): self + { + $this->sqref = $str; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index 7053a5a5c7..b699cb5741 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -45,6 +45,7 @@ public function load(): void $docValidation->setPrompt((string) $dataValidation['prompt']); $docValidation->setFormula1((string) $dataValidation->formula1); $docValidation->setFormula2((string) $dataValidation->formula2); + $docValidation->setSqref($range); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php index 9b3e47edd0..b2bc99f052 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php @@ -27,6 +27,7 @@ public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet public function load(): void { + $this->topLeft(); $this->zoomScale(); $this->view(); $this->gridLines(); @@ -74,6 +75,13 @@ private function view(): void } } + private function topLeft(): void + { + if (isset($this->sheetViewAttributes->topLeftCell)) { + $this->worksheet->setTopLeftCell($this->sheetViewAttributes->topLeftCell); + } + } + private function gridLines(): void { if (isset($this->sheetViewAttributes->showGridLines)) { diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index da31fa8b40..8dd16df033 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1966,6 +1966,13 @@ public function freezePane($cell, $topLeftCell = null) return $this; } + public function setTopLeftCell(string $topLeftCell): self + { + $this->topLeftCell = $topLeftCell; + + return $this; + } + /** * Freeze Pane by using numeric cell coordinates. * diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 58e6c06496..98e2ae42b2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -85,8 +85,8 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable = // conditionalFormatting $this->writeConditionalFormatting($objWriter, $pSheet); - // dataValidations - $this->writeDataValidations($objWriter, $pSheet); + // dataValidations moved to end + //$this->writeDataValidations($objWriter, $pSheet); // hyperlinks $this->writeHyperlinks($objWriter, $pSheet); @@ -121,6 +121,8 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable = // ConditionalFormattingRuleExtensionList // (Must be inserted last. Not insert last, an Excel parse error will occur) $this->writeExtLst($objWriter, $pSheet); + // dataValidations + $this->writeDataValidations($objWriter, $pSheet); $objWriter->endElement(); @@ -143,7 +145,7 @@ private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSh if (!$pSheet->hasCodeName()) { $pSheet->setCodeName($pSheet->getTitle()); } - $objWriter->writeAttribute('codeName', $pSheet->getCodeName()); + self::writeAttributeNotNull($objWriter, 'codeName', $pSheet->getCodeName()); } $autoFilterRange = $pSheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { @@ -247,6 +249,7 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $ $objWriter->writeAttribute('rightToLeft', 'true'); } + $topLeftCell = $pSheet->getTopLeftCell(); $activeCell = $pSheet->getActiveCell(); $sqref = $pSheet->getSelectedCells(); @@ -258,8 +261,6 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $ --$xSplit; --$ySplit; - $topLeftCell = $pSheet->getTopLeftCell(); - // pane $pane = 'topRight'; $objWriter->startElement('pane'); @@ -270,7 +271,7 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $ $objWriter->writeAttribute('ySplit', $ySplit); $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft'; } - $objWriter->writeAttribute('topLeftCell', $topLeftCell); + self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell); $objWriter->writeAttribute('activePane', $pane); $objWriter->writeAttribute('state', 'frozen'); $objWriter->endElement(); @@ -284,6 +285,8 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $ $objWriter->writeAttribute('pane', 'bottomLeft'); $objWriter->endElement(); } + } else { + self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell); } // Selection @@ -467,6 +470,13 @@ private static function writeAttributeIf(XMLWriter $objWriter, $condition, strin } } + private static function writeAttributeNotNull(XMLWriter $objWriter, string $attr, ?string $val): void + { + if ($val !== null) { + $objWriter->writeAttribute($attr, $val); + } + } + private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void { if ($condition) { @@ -680,11 +690,16 @@ private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksh // Write data validations? if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); - $objWriter->startElement('dataValidations'); + $objWriter->startElement('extLst'); + $objWriter->startElement('ext'); + $objWriter->writeAttribute('uri', '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}'); + $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'); + $objWriter->startElement('x14:dataValidations'); $objWriter->writeAttribute('count', count($dataValidationCollection)); + $objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main'); foreach ($dataValidationCollection as $coordinate => $dv) { - $objWriter->startElement('dataValidation'); + $objWriter->startElement('x14:dataValidation'); if ($dv->getType() != '') { $objWriter->writeAttribute('type', $dv->getType()); @@ -717,19 +732,24 @@ private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksh $objWriter->writeAttribute('prompt', $dv->getPrompt()); } - $objWriter->writeAttribute('sqref', $coordinate); - if ($dv->getFormula1() !== '') { - $objWriter->writeElement('formula1', $dv->getFormula1()); + $objWriter->startElement('x14:formula1'); + $objWriter->writeElement('xm:f', $dv->getFormula1()); + $objWriter->endElement(); } if ($dv->getFormula2() !== '') { - $objWriter->writeElement('formula2', $dv->getFormula2()); + $objWriter->startElement('x14:formula2'); + $objWriter->writeElement('xm:f', $dv->getFormula2()); + $objWriter->endElement(); } + $objWriter->writeElement('xm:sqref', $dv->getSqref() ?? $coordinate); $objWriter->endElement(); } - $objWriter->endElement(); + $objWriter->endElement(); // dataValidations + $objWriter->endElement(); // ext + $objWriter->endElement(); // extLst } } diff --git a/tests/PhpSpreadsheetTests/Cell/DataValidator2Test.php b/tests/PhpSpreadsheetTests/Cell/DataValidator2Test.php new file mode 100644 index 0000000000..9153a5b811 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/DataValidator2Test.php @@ -0,0 +1,75 @@ +load('tests/data/Reader/XLSX/issue.1432b.xlsx'); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('H1', $sheet->getTopLeftCell()); + self::assertSame('K3', $sheet->getSelectedCells()); + + $testCell = $sheet->getCell('K3'); + $validation = $testCell->getDataValidation(); + self::assertSame(DataValidation::TYPE_LIST, $validation->getType()); + + $testCell = $sheet->getCell('R2'); + $validation = $testCell->getDataValidation(); + self::assertSame(DataValidation::TYPE_LIST, $validation->getType()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + + $cell = 'K3'; + $testCell = $sheet->getCell($cell); + $validation = $testCell->getDataValidation(); + self::assertSame(DataValidation::TYPE_LIST, $validation->getType()); + $testCell->setValue('Y'); + self::assertTrue($testCell->hasValidValue(), 'K3 other sheet has valid value'); + $testCell = $sheet->getCell($cell); + $testCell->setValue('X'); + self::assertFalse($testCell->hasValidValue(), 'K3 other sheet has invalid value'); + + $cell = 'J2'; + $testCell = $sheet->getCell($cell); + $validation = $testCell->getDataValidation(); + self::assertSame(DataValidation::TYPE_LIST, $validation->getType()); + $testCell = $sheet->getCell($cell); + $testCell->setValue('GBP'); + self::assertTrue($testCell->hasValidValue(), 'J2 other sheet has valid value'); + $testCell = $sheet->getCell($cell); + $testCell->setValue('XYZ'); + self::assertFalse($testCell->hasValidValue(), 'J2 other sheet has invalid value'); + + $cell = 'R2'; + $testCell = $sheet->getCell($cell); + $validation = $testCell->getDataValidation(); + self::assertSame(DataValidation::TYPE_LIST, $validation->getType()); + $testCell->setValue('ListItem2'); + self::assertTrue($testCell->hasValidValue(), 'R2 same sheet has valid value'); + $testCell = $sheet->getCell($cell); + $testCell->setValue('ListItem99'); + self::assertFalse($testCell->hasValidValue(), 'R2 same sheet has invalid value'); + + $styles = $sheet->getConditionalStyles('I1:I1048576'); + self::assertCount(1, $styles); + $style = $styles[0]; + self::assertSame(Conditional::CONDITION_CELLIS, $style->getConditionType()); + self::assertSame(Conditional::OPERATOR_BETWEEN, $style->getOperatorType()); + $conditions = $style->getConditions(); + self::assertSame('10', $conditions[0]); + self::assertSame('20', $conditions[1]); + self::assertSame('FF70AD47', $style->getStyle()->getFill()->getEndColor()->getARGB()); + + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.1432b.xlsx b/tests/data/Reader/XLSX/issue.1432b.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0762c24dc4bc4b44ddae16de75895bfb483631ea GIT binary patch literal 12936 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 bi2o4)6lEZRmi${i2ob~@2nHzT-&X$v2)3*m literal 0 HcmV?d00001