Skip to content

Commit e1ae687

Browse files
authored
Merge pull request #4240 from oleibman/issue797
Breaking Change for DataValidation
2 parents e721975 + d93a707 commit e1ae687

22 files changed

+890
-253
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org).
77

88
## TBD - 4.0.0
99

10+
### BREAKING CHANGES
11+
12+
- Data Validations will be stored by worksheet, not cell. Index can be one or more cells or cell ranges. [Issue #797](https://github.com/PHPOffice/PhpSpreadsheet/issues/797) [Issue #4091](https://github.com/PHPOffice/PhpSpreadsheet/issues/4091) [Issue #4206](https://github.com/PHPOffice/PhpSpreadsheet/issues/4206) [PR #4240](https://github.com/PHPOffice/PhpSpreadsheet/pull/4240)
13+
- Conditional Formatting adds Priority property and handles overlapping ranges better. [Issue #4312](https://github.com/PHPOffice/PhpSpreadsheet/issues/4312) [Issue #4318](https://github.com/PHPOffice/PhpSpreadsheet/issues/4318) [PR #4314](https://github.com/PHPOffice/PhpSpreadsheet/pull/4314)
14+
- Deletion of items deprecated in Release 3. See "removed" below.
15+
1016
### Added
1117

1218
- Pdf Charts and Drawings. [Discussion #4129](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4129) [Discussion #4168](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4168) [PR #4327](https://github.com/PHPOffice/PhpSpreadsheet/pull/4327)
1319

20+
### Removed
21+
22+
- Nothing yet.
23+
1424
### Changed
1525

1626
- Nothing yet.

docs/topics/recipes.md

+29
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,8 @@ directly in some cell range, say A1:A3, and instead use, say,
15631563
`$validation->setFormula1('\'Sheet title\'!$A$1:$A$3')`. Another benefit is that
15641564
the item values themselves can contain the comma `,` character itself.
15651565

1566+
### Setting Validation on Multiple Cells - Release 3 and Below
1567+
15661568
If you need data validation on multiple cells, one can clone the
15671569
ruleset:
15681570

@@ -1575,6 +1577,33 @@ Alternatively, one can apply the validation to a range of cells:
15751577
$validation->setSqref('B5:B1048576');
15761578
```
15771579

1580+
### Setting Validation on Multiple Cells - Release 4 and Above
1581+
1582+
Starting with Release 4, Data Validation can be set simultaneously on several cells/cell ranges.
1583+
1584+
```php
1585+
$spreadsheet->getActiveSheet()->getDataValidation('A1:A4 D5 E6:E7')
1586+
->set...(...);
1587+
```
1588+
1589+
In theory, this means that more than one Data Validation can apply to a cell.
1590+
It appears that, when Excel reads a spreadsheet with more than one Data Validation applying to a cell,
1591+
whichever appears first in the Xml is what Xml uses.
1592+
PhpSpreadsheet will instead apply a DatValidation applying to a single cell first;
1593+
then, if it doesn't find such a match, it will use the first applicable definition which is read (or created after or in lieu of reading).
1594+
This allows you, for example, to set Data Validation on all but a few cells in a column:
1595+
```php
1596+
$dv = new DataValidation();
1597+
$dv->setType(DataValidation::TYPE_NONE);
1598+
$sheet->setDataValidation('A5:A7', $dv);
1599+
$dv = new DataValidation();
1600+
$dv->set...(...);
1601+
$sheet->setDataValidation('A:A', $dv);
1602+
$dv = new DataValidation();
1603+
$dv->setType(DataValidation::TYPE_NONE);
1604+
$sheet->setDataValidation('A9', $dv);
1605+
```
1606+
15781607
## Setting a column's width
15791608

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

src/PhpSpreadsheet/Cell/Coordinate.php

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Cell;
44

55
use PhpOffice\PhpSpreadsheet\Exception;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Validations;
67
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
78

89
/**
@@ -306,6 +307,7 @@ private static function validateReferenceAndGetData($reference): array
306307
*/
307308
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
308309
{
310+
$range = Validations::convertWholeRowColumn($range);
309311
$rangeData = self::validateReferenceAndGetData($range);
310312
if ($rangeData['type'] === 'invalid') {
311313
throw new Exception('First argument needs to be a range');

src/PhpSpreadsheet/Cell/DataValidation.php

-22
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,6 @@ class DataValidation
9595
*/
9696
private string $prompt = '';
9797

98-
/**
99-
* Create a new DataValidation.
100-
*/
101-
public function __construct()
102-
{
103-
}
104-
10598
/**
10699
* Get Formula 1.
107100
*/
@@ -390,21 +383,6 @@ public function getHashCode(): string
390383
);
391384
}
392385

393-
/**
394-
* Implement PHP __clone to create a deep clone, not just a shallow copy.
395-
*/
396-
public function __clone()
397-
{
398-
$vars = get_object_vars($this);
399-
foreach ($vars as $key => $value) {
400-
if (is_object($value)) {
401-
$this->$key = clone $value;
402-
} else {
403-
$this->$key = $value;
404-
}
405-
}
406-
}
407-
408386
private ?string $sqref = null;
409387

410388
public function getSqref(): ?string

src/PhpSpreadsheet/Cell/DataValidator.php

+62-38
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Cell;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6-
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
77
use PhpOffice\PhpSpreadsheet\Exception;
88

99
/**
@@ -37,46 +37,70 @@ public function isValid(Cell $cell): bool
3737
if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) {
3838
$returnValue = false;
3939
} else {
40-
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue);
40+
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue, $cell);
4141
}
4242
} elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) {
4343
if (!is_numeric($cellValue)) {
4444
$returnValue = false;
4545
} else {
46-
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue);
46+
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue, $cell);
4747
}
4848
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
49-
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()));
49+
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()), $cell);
5050
}
5151

5252
return $returnValue;
5353
}
5454

55-
private function numericOperator(DataValidation $dataValidation, int|float $cellValue): bool
55+
private const TWO_FORMULAS = [DataValidation::OPERATOR_BETWEEN, DataValidation::OPERATOR_NOTBETWEEN];
56+
57+
private static function evaluateNumericFormula(mixed $formula, Cell $cell): mixed
58+
{
59+
if (!is_numeric($formula)) {
60+
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
61+
62+
try {
63+
$result = $calculation
64+
->calculateFormula("=$formula", $cell->getCoordinate(), $cell);
65+
while (is_array($result)) {
66+
$result = array_pop($result);
67+
}
68+
$formula = $result;
69+
} catch (Exception) {
70+
// do nothing
71+
}
72+
}
73+
74+
return $formula;
75+
}
76+
77+
private function numericOperator(DataValidation $dataValidation, int|float $cellValue, Cell $cell): bool
5678
{
5779
$operator = $dataValidation->getOperator();
58-
$formula1 = $dataValidation->getFormula1();
59-
$formula2 = $dataValidation->getFormula2();
60-
$returnValue = false;
61-
if ($operator === DataValidation::OPERATOR_BETWEEN) {
62-
$returnValue = $cellValue >= $formula1 && $cellValue <= $formula2;
63-
} elseif ($operator === DataValidation::OPERATOR_NOTBETWEEN) {
64-
$returnValue = $cellValue < $formula1 || $cellValue > $formula2;
65-
} elseif ($operator === DataValidation::OPERATOR_EQUAL) {
66-
$returnValue = $cellValue == $formula1;
67-
} elseif ($operator === DataValidation::OPERATOR_NOTEQUAL) {
68-
$returnValue = $cellValue != $formula1;
69-
} elseif ($operator === DataValidation::OPERATOR_LESSTHAN) {
70-
$returnValue = $cellValue < $formula1;
71-
} elseif ($operator === DataValidation::OPERATOR_LESSTHANOREQUAL) {
72-
$returnValue = $cellValue <= $formula1;
73-
} elseif ($operator === DataValidation::OPERATOR_GREATERTHAN) {
74-
$returnValue = $cellValue > $formula1;
75-
} elseif ($operator === DataValidation::OPERATOR_GREATERTHANOREQUAL) {
76-
$returnValue = $cellValue >= $formula1;
80+
$formula1 = self::evaluateNumericFormula(
81+
$dataValidation->getFormula1(),
82+
$cell
83+
);
84+
85+
$formula2 = 0;
86+
if (in_array($operator, self::TWO_FORMULAS, true)) {
87+
$formula2 = self::evaluateNumericFormula(
88+
$dataValidation->getFormula2(),
89+
$cell
90+
);
7791
}
7892

79-
return $returnValue;
93+
return match ($operator) {
94+
DataValidation::OPERATOR_BETWEEN => $cellValue >= $formula1 && $cellValue <= $formula2,
95+
DataValidation::OPERATOR_NOTBETWEEN => $cellValue < $formula1 || $cellValue > $formula2,
96+
DataValidation::OPERATOR_EQUAL => $cellValue == $formula1,
97+
DataValidation::OPERATOR_NOTEQUAL => $cellValue != $formula1,
98+
DataValidation::OPERATOR_LESSTHAN => $cellValue < $formula1,
99+
DataValidation::OPERATOR_LESSTHANOREQUAL => $cellValue <= $formula1,
100+
DataValidation::OPERATOR_GREATERTHAN => $cellValue > $formula1,
101+
DataValidation::OPERATOR_GREATERTHANOREQUAL => $cellValue >= $formula1,
102+
default => false,
103+
};
80104
}
81105

82106
/**
@@ -94,22 +118,22 @@ private function isValueInList(Cell $cell): bool
94118
// inline values list
95119
if ($formula1[0] === '"') {
96120
return in_array(strtolower($cellValueString), explode(',', strtolower(trim($formula1, '"'))), true);
97-
} elseif (strpos($formula1, ':') > 0) {
98-
// values list cells
99-
$matchFormula = '=MATCH(' . $cell->getCoordinate() . ', ' . $formula1 . ', 0)';
100-
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
101-
102-
try {
103-
$result = $calculation->calculateFormula($matchFormula, $cell->getCoordinate(), $cell);
104-
while (is_array($result)) {
105-
$result = array_pop($result);
106-
}
121+
}
122+
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
107123

108-
return $result !== ExcelError::NA();
109-
} catch (Exception) {
110-
return false;
124+
try {
125+
$result = $calculation->calculateFormula("=$formula1", $cell->getCoordinate(), $cell);
126+
$result = is_array($result) ? Functions::flattenArray($result) : [$result];
127+
foreach ($result as $oneResult) {
128+
if (is_scalar($oneResult) && strcasecmp((string) $oneResult, $cellValueString) === 0) {
129+
return true;
130+
}
111131
}
132+
} catch (Exception) {
133+
// do nothing
112134
}
135+
136+
return false;
113137
}
114138

115139
return true;

src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php

+36-18
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
44

5-
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
5+
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
66
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
77
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
88
use PhpOffice\PhpSpreadsheet\Reader\Xls;
9+
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet as XlsWorksheet;
910

1011
class DataValidationHelper extends Xls
1112
{
@@ -176,25 +177,42 @@ protected function readDataValidation2(Xls $xls): void
176177
// offset: var; size: var; cell range address list with
177178
$cellRangeAddressList = Biff8::readBIFF8CellRangeAddressList(substr($recordData, $offset));
178179
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
180+
$maxRow = (string) AddressRange::MAX_ROW;
181+
$maxCol = AddressRange::MAX_COLUMN;
182+
$maxXlsRow = (string) XlsWorksheet::MAX_XLS_ROW;
183+
$maxXlsColumnString = (string) XlsWorksheet::MAX_XLS_COLUMN_STRING;
179184

180185
foreach ($cellRangeAddresses as $cellRange) {
181-
$stRange = $xls->phpSheet->shrinkRangeToFit($cellRange);
182-
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
183-
$objValidation = $xls->phpSheet->getCell($coordinate)->getDataValidation();
184-
$objValidation->setType($type);
185-
$objValidation->setErrorStyle($errorStyle);
186-
$objValidation->setAllowBlank((bool) $allowBlank);
187-
$objValidation->setShowInputMessage((bool) $showInputMessage);
188-
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
189-
$objValidation->setShowDropDown(!$suppressDropDown);
190-
$objValidation->setOperator($operator);
191-
$objValidation->setErrorTitle($errorTitle);
192-
$objValidation->setError($error);
193-
$objValidation->setPromptTitle($promptTitle);
194-
$objValidation->setPrompt($prompt);
195-
$objValidation->setFormula1($formula1);
196-
$objValidation->setFormula2($formula2);
197-
}
186+
$cellRange = preg_replace(
187+
[
188+
"/([a-z]+)1:([a-z]+)$maxXlsRow/i",
189+
"/([a-z]+\\d+):([a-z]+)$maxXlsRow/i",
190+
"/A(\\d+):$maxXlsColumnString(\\d+)/i",
191+
"/([a-z]+\\d+):$maxXlsColumnString(\\d+)/i",
192+
],
193+
[
194+
'$1:$2',
195+
'$1:${2}' . $maxRow,
196+
'$1:$2',
197+
'$1:' . $maxCol . '$2',
198+
],
199+
$cellRange
200+
) ?? $cellRange;
201+
$objValidation = new DataValidation();
202+
$objValidation->setType($type);
203+
$objValidation->setErrorStyle($errorStyle);
204+
$objValidation->setAllowBlank((bool) $allowBlank);
205+
$objValidation->setShowInputMessage((bool) $showInputMessage);
206+
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
207+
$objValidation->setShowDropDown(!$suppressDropDown);
208+
$objValidation->setOperator($operator);
209+
$objValidation->setErrorTitle($errorTitle);
210+
$objValidation->setError($error);
211+
$objValidation->setPromptTitle($promptTitle);
212+
$objValidation->setPrompt($prompt);
213+
$objValidation->setFormula1($formula1);
214+
$objValidation->setFormula2($formula2);
215+
$xls->phpSheet->setDataValidation($cellRange, $objValidation);
198216
}
199217
}
200218
}

src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php

+17-25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
44

55
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6+
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
67
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
78
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
89
use SimpleXMLElement;
@@ -36,31 +37,22 @@ public function load(): void
3637
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
3738
// Uppercase coordinate
3839
$range = strtoupper((string) $dataValidation['sqref']);
39-
$rangeSet = explode(' ', $range);
40-
foreach ($rangeSet as $range) {
41-
$stRange = $this->worksheet->shrinkRangeToFit($range);
42-
43-
// Extract all cell references in $range
44-
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $reference) {
45-
// Create validation
46-
$docValidation = $this->worksheet->getCell($reference)->getDataValidation();
47-
$docValidation->setType((string) $dataValidation['type']);
48-
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
49-
$docValidation->setOperator((string) $dataValidation['operator']);
50-
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
51-
// showDropDown is inverted (works as hideDropDown if true)
52-
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
53-
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
54-
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
55-
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
56-
$docValidation->setError((string) $dataValidation['error']);
57-
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
58-
$docValidation->setPrompt((string) $dataValidation['prompt']);
59-
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
60-
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
61-
$docValidation->setSqref($range);
62-
}
63-
}
40+
$docValidation = new DataValidation();
41+
$docValidation->setType((string) $dataValidation['type']);
42+
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
43+
$docValidation->setOperator((string) $dataValidation['operator']);
44+
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
45+
// showDropDown is inverted (works as hideDropDown if true)
46+
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
47+
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
48+
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
49+
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
50+
$docValidation->setError((string) $dataValidation['error']);
51+
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
52+
$docValidation->setPrompt((string) $dataValidation['prompt']);
53+
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
54+
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
55+
$this->worksheet->setDataValidation($range, $docValidation);
6456
}
6557
}
6658
}

0 commit comments

Comments
 (0)