Skip to content

Commit 16bd7b9

Browse files
committed
Data Validations Referencing Another Sheet
See issues PHPOffice#1432 and PHPOffice#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 PHPOffice#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 PHPOffice#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.
1 parent 3c5750b commit 16bd7b9

File tree

9 files changed

+144
-18
lines changed

9 files changed

+144
-18
lines changed

docs/topics/recipes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,11 @@ ruleset:
11141114
$spreadsheet->getActiveSheet()->getCell('B8')->setDataValidation(clone $validation);
11151115
```
11161116

1117+
Alternatively, one can apply the validation to a range of cells:
1118+
```php
1119+
$validation->setSqref('B5:B1048576');
1120+
```
1121+
11171122
## Setting a column's width
11181123

11191124
A column's width can be set using the following code:

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6315,11 +6315,6 @@ parameters:
63156315
count: 1
63166316
path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
63176317

6318-
-
6319-
message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#"
6320-
count: 2
6321-
path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
6322-
63236318
-
63246319
message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#"
63256320
count: 19

src/PhpSpreadsheet/Cell/DataValidation.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,19 @@ public function __clone()
478478
}
479479
}
480480
}
481+
482+
/** @var ?string */
483+
private $sqref;
484+
485+
public function getSqref(): ?string
486+
{
487+
return $this->sqref;
488+
}
489+
490+
public function setSqref(?string $str): self
491+
{
492+
$this->sqref = $str;
493+
494+
return $this;
495+
}
481496
}

src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function load(): void
4545
$docValidation->setPrompt((string) $dataValidation['prompt']);
4646
$docValidation->setFormula1((string) $dataValidation->formula1);
4747
$docValidation->setFormula2((string) $dataValidation->formula2);
48+
$docValidation->setSqref($range);
4849
}
4950
}
5051
}

src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet
2727

2828
public function load(): void
2929
{
30+
$this->topLeft();
3031
$this->zoomScale();
3132
$this->view();
3233
$this->gridLines();
@@ -74,6 +75,13 @@ private function view(): void
7475
}
7576
}
7677

78+
private function topLeft(): void
79+
{
80+
if (isset($this->sheetViewAttributes->topLeftCell)) {
81+
$this->worksheet->setTopLeftCell($this->sheetViewAttributes->topLeftCell);
82+
}
83+
}
84+
7785
private function gridLines(): void
7886
{
7987
if (isset($this->sheetViewAttributes->showGridLines)) {

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,6 +1966,13 @@ public function freezePane($cell, $topLeftCell = null)
19661966
return $this;
19671967
}
19681968

1969+
public function setTopLeftCell(string $topLeftCell): self
1970+
{
1971+
$this->topLeftCell = $topLeftCell;
1972+
1973+
return $this;
1974+
}
1975+
19691976
/**
19701977
* Freeze Pane by using numeric cell coordinates.
19711978
*

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable =
8585
// conditionalFormatting
8686
$this->writeConditionalFormatting($objWriter, $pSheet);
8787

88-
// dataValidations
89-
$this->writeDataValidations($objWriter, $pSheet);
88+
// dataValidations moved to end
89+
//$this->writeDataValidations($objWriter, $pSheet);
9090

9191
// hyperlinks
9292
$this->writeHyperlinks($objWriter, $pSheet);
@@ -121,6 +121,8 @@ public function writeWorksheet(PhpspreadsheetWorksheet $pSheet, $pStringTable =
121121
// ConditionalFormattingRuleExtensionList
122122
// (Must be inserted last. Not insert last, an Excel parse error will occur)
123123
$this->writeExtLst($objWriter, $pSheet);
124+
// dataValidations
125+
$this->writeDataValidations($objWriter, $pSheet);
124126

125127
$objWriter->endElement();
126128

@@ -143,7 +145,7 @@ private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSh
143145
if (!$pSheet->hasCodeName()) {
144146
$pSheet->setCodeName($pSheet->getTitle());
145147
}
146-
$objWriter->writeAttribute('codeName', $pSheet->getCodeName());
148+
self::writeAttributeNotNull($objWriter, 'codeName', $pSheet->getCodeName());
147149
}
148150
$autoFilterRange = $pSheet->getAutoFilter()->getRange();
149151
if (!empty($autoFilterRange)) {
@@ -247,6 +249,7 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
247249
$objWriter->writeAttribute('rightToLeft', 'true');
248250
}
249251

252+
$topLeftCell = $pSheet->getTopLeftCell();
250253
$activeCell = $pSheet->getActiveCell();
251254
$sqref = $pSheet->getSelectedCells();
252255

@@ -258,8 +261,6 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
258261
--$xSplit;
259262
--$ySplit;
260263

261-
$topLeftCell = $pSheet->getTopLeftCell();
262-
263264
// pane
264265
$pane = 'topRight';
265266
$objWriter->startElement('pane');
@@ -270,7 +271,7 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
270271
$objWriter->writeAttribute('ySplit', $ySplit);
271272
$pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft';
272273
}
273-
$objWriter->writeAttribute('topLeftCell', $topLeftCell);
274+
self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
274275
$objWriter->writeAttribute('activePane', $pane);
275276
$objWriter->writeAttribute('state', 'frozen');
276277
$objWriter->endElement();
@@ -284,6 +285,8 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
284285
$objWriter->writeAttribute('pane', 'bottomLeft');
285286
$objWriter->endElement();
286287
}
288+
} else {
289+
self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
287290
}
288291

289292
// Selection
@@ -467,6 +470,13 @@ private static function writeAttributeIf(XMLWriter $objWriter, $condition, strin
467470
}
468471
}
469472

473+
private static function writeAttributeNotNull(XMLWriter $objWriter, string $attr, ?string $val): void
474+
{
475+
if ($val !== null) {
476+
$objWriter->writeAttribute($attr, $val);
477+
}
478+
}
479+
470480
private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void
471481
{
472482
if ($condition) {
@@ -680,11 +690,16 @@ private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksh
680690
// Write data validations?
681691
if (!empty($dataValidationCollection)) {
682692
$dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection);
683-
$objWriter->startElement('dataValidations');
693+
$objWriter->startElement('extLst');
694+
$objWriter->startElement('ext');
695+
$objWriter->writeAttribute('uri', '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}');
696+
$objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main');
697+
$objWriter->startElement('x14:dataValidations');
684698
$objWriter->writeAttribute('count', count($dataValidationCollection));
699+
$objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main');
685700

686701
foreach ($dataValidationCollection as $coordinate => $dv) {
687-
$objWriter->startElement('dataValidation');
702+
$objWriter->startElement('x14:dataValidation');
688703

689704
if ($dv->getType() != '') {
690705
$objWriter->writeAttribute('type', $dv->getType());
@@ -717,19 +732,24 @@ private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksh
717732
$objWriter->writeAttribute('prompt', $dv->getPrompt());
718733
}
719734

720-
$objWriter->writeAttribute('sqref', $coordinate);
721-
722735
if ($dv->getFormula1() !== '') {
723-
$objWriter->writeElement('formula1', $dv->getFormula1());
736+
$objWriter->startElement('x14:formula1');
737+
$objWriter->writeElement('xm:f', $dv->getFormula1());
738+
$objWriter->endElement();
724739
}
725740
if ($dv->getFormula2() !== '') {
726-
$objWriter->writeElement('formula2', $dv->getFormula2());
741+
$objWriter->startElement('x14:formula2');
742+
$objWriter->writeElement('xm:f', $dv->getFormula2());
743+
$objWriter->endElement();
727744
}
745+
$objWriter->writeElement('xm:sqref', $dv->getSqref() ?? $coordinate);
728746

729747
$objWriter->endElement();
730748
}
731749

732-
$objWriter->endElement();
750+
$objWriter->endElement(); // dataValidations
751+
$objWriter->endElement(); // ext
752+
$objWriter->endElement(); // extLst
733753
}
734754
}
735755

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Cell;
4+
5+
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
6+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
7+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class DataValidator2Test extends AbstractFunctional
11+
{
12+
public function testList(): void
13+
{
14+
$reader = new Xlsx();
15+
$spreadsheet = $reader->load('tests/data/Reader/XLSX/issue.1432b.xlsx');
16+
$sheet = $spreadsheet->getActiveSheet();
17+
self::assertSame('H1', $sheet->getTopLeftCell());
18+
self::assertSame('K3', $sheet->getSelectedCells());
19+
20+
$testCell = $sheet->getCell('K3');
21+
$validation = $testCell->getDataValidation();
22+
self::assertSame(DataValidation::TYPE_LIST, $validation->getType());
23+
24+
$testCell = $sheet->getCell('R2');
25+
$validation = $testCell->getDataValidation();
26+
self::assertSame(DataValidation::TYPE_LIST, $validation->getType());
27+
28+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
29+
$sheet = $reloadedSpreadsheet->getActiveSheet();
30+
31+
$cell = 'K3';
32+
$testCell = $sheet->getCell($cell);
33+
$validation = $testCell->getDataValidation();
34+
self::assertSame(DataValidation::TYPE_LIST, $validation->getType());
35+
$testCell->setValue('Y');
36+
self::assertTrue($testCell->hasValidValue(), 'K3 other sheet has valid value');
37+
$testCell = $sheet->getCell($cell);
38+
$testCell->setValue('X');
39+
self::assertFalse($testCell->hasValidValue(), 'K3 other sheet has invalid value');
40+
41+
$cell = 'J2';
42+
$testCell = $sheet->getCell($cell);
43+
$validation = $testCell->getDataValidation();
44+
self::assertSame(DataValidation::TYPE_LIST, $validation->getType());
45+
$testCell = $sheet->getCell($cell);
46+
$testCell->setValue('GBP');
47+
self::assertTrue($testCell->hasValidValue(), 'J2 other sheet has valid value');
48+
$testCell = $sheet->getCell($cell);
49+
$testCell->setValue('XYZ');
50+
self::assertFalse($testCell->hasValidValue(), 'J2 other sheet has invalid value');
51+
52+
$cell = 'R2';
53+
$testCell = $sheet->getCell($cell);
54+
$validation = $testCell->getDataValidation();
55+
self::assertSame(DataValidation::TYPE_LIST, $validation->getType());
56+
$testCell->setValue('ListItem2');
57+
self::assertTrue($testCell->hasValidValue(), 'R2 same sheet has valid value');
58+
$testCell = $sheet->getCell($cell);
59+
$testCell->setValue('ListItem99');
60+
self::assertFalse($testCell->hasValidValue(), 'R2 same sheet has invalid value');
61+
62+
$styles = $sheet->getConditionalStyles('I1:I1048576');
63+
self::assertCount(1, $styles);
64+
$style = $styles[0];
65+
self::assertSame(Conditional::CONDITION_CELLIS, $style->getConditionType());
66+
self::assertSame(Conditional::OPERATOR_BETWEEN, $style->getOperatorType());
67+
$conditions = $style->getConditions();
68+
self::assertSame('10', $conditions[0]);
69+
self::assertSame('20', $conditions[1]);
70+
self::assertSame('FF70AD47', $style->getStyle()->getFill()->getEndColor()->getARGB());
71+
72+
$spreadsheet->disconnectWorksheets();
73+
$reloadedSpreadsheet->disconnectWorksheets();
74+
}
75+
}
12.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)