Skip to content

Commit

Permalink
Data Validations Referencing Another Sheet (#2265)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oleibman authored Aug 24, 2021
1 parent 710f9f1 commit de5f450
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 18 deletions.
5 changes: 5 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6130,11 +6130,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
Expand Down
15 changes: 15 additions & 0 deletions src/PhpSpreadsheet/Cell/DataValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
1 change: 1 addition & 0 deletions src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet

public function load(): void
{
$this->topLeft();
$this->zoomScale();
$this->view();
$this->gridLines();
Expand Down Expand Up @@ -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)) {
Expand Down
7 changes: 7 additions & 0 deletions src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
46 changes: 33 additions & 13 deletions src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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)) {
Expand Down Expand Up @@ -247,6 +249,7 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
$objWriter->writeAttribute('rightToLeft', 'true');
}

$topLeftCell = $pSheet->getTopLeftCell();
$activeCell = $pSheet->getActiveCell();
$sqref = $pSheet->getSelectedCells();

Expand All @@ -258,8 +261,6 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
--$xSplit;
--$ySplit;

$topLeftCell = $pSheet->getTopLeftCell();

// pane
$pane = 'topRight';
$objWriter->startElement('pane');
Expand All @@ -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();
Expand All @@ -284,6 +285,8 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
$objWriter->writeAttribute('pane', 'bottomLeft');
$objWriter->endElement();
}
} else {
self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
}

// Selection
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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
}
}

Expand Down
75 changes: 75 additions & 0 deletions tests/PhpSpreadsheetTests/Cell/DataValidator2Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Cell;

use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class DataValidator2Test extends AbstractFunctional
{
public function testList(): void
{
$reader = new Xlsx();
$spreadsheet = $reader->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();
}
}
Binary file added tests/data/Reader/XLSX/issue.1432b.xlsx
Binary file not shown.

0 comments on commit de5f450

Please sign in to comment.