Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking Change for DataValidation #4240

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
29 changes: 29 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,8 @@ directly in some cell range, say A1:A3, and instead use, say,
`$validation->setFormula1('\'Sheet title\'!$A$1:$A$3')`. Another benefit is that
the item values themselves can contain the comma `,` character itself.

### Setting Validation on Multiple Cells - Release 3 and Below

If you need data validation on multiple cells, one can clone the
ruleset:

Expand All @@ -1572,6 +1574,33 @@ Alternatively, one can apply the validation to a range of cells:
$validation->setSqref('B5:B1048576');
```

### Setting Validation on Multiple Cells - Release 4 and Above

Starting with Release 4, Data Validation can be set simultaneously on several cells/cell ranges.

```php
$spreadsheet->getActiveSheet()->getDataValidation('A1:A4 D5 E6:E7')
->set...(...);
```

In theory, this means that more than one Data Validation can apply to a cell.
It appears that, when Excel reads a spreadsheet with more than one Data Validation applying to a cell,
whichever appears first in the Xml is what Xml uses.
PhpSpreadsheet will instead apply a DatValidation applying to a single cell first;
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).
This allows you, for example, to set Data Validation on all but a few cells in a column:
```php
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A5:A7', $dv);
$dv = new DataValidation();
$dv->set...(...);
$sheet->setDataValidation('A:A', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A9', $dv);
```

## Setting a column's width

A column's width can be set using the following code:
Expand Down
2 changes: 2 additions & 0 deletions src/PhpSpreadsheet/Cell/Coordinate.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Cell;

use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Validations;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

/**
Expand Down Expand Up @@ -306,6 +307,7 @@ private static function validateReferenceAndGetData($reference): array
*/
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
{
$range = Validations::convertWholeRowColumn($range);
$rangeData = self::validateReferenceAndGetData($range);
if ($rangeData['type'] === 'invalid') {
throw new Exception('First argument needs to be a range');
Expand Down
22 changes: 0 additions & 22 deletions src/PhpSpreadsheet/Cell/DataValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,6 @@ class DataValidation
*/
private string $prompt = '';

/**
* Create a new DataValidation.
*/
public function __construct()
{
}

/**
* Get Formula 1.
*/
Expand Down Expand Up @@ -390,21 +383,6 @@ public function getHashCode(): string
);
}

/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
$this->$key = clone $value;
} else {
$this->$key = $value;
}
}
}

private ?string $sqref = null;

public function getSqref(): ?string
Expand Down
100 changes: 62 additions & 38 deletions src/PhpSpreadsheet/Cell/DataValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Cell;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Exception;

/**
Expand Down Expand Up @@ -37,46 +37,70 @@ public function isValid(Cell $cell): bool
if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue);
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue, $cell);
}
} elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) {
if (!is_numeric($cellValue)) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue);
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue, $cell);
}
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()));
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()), $cell);
}

return $returnValue;
}

private function numericOperator(DataValidation $dataValidation, int|float $cellValue): bool
private const TWO_FORMULAS = [DataValidation::OPERATOR_BETWEEN, DataValidation::OPERATOR_NOTBETWEEN];

private static function evaluateNumericFormula(mixed $formula, Cell $cell): mixed
{
if (!is_numeric($formula)) {
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

try {
$result = $calculation
->calculateFormula("=$formula", $cell->getCoordinate(), $cell);
while (is_array($result)) {
$result = array_pop($result);
}
$formula = $result;
} catch (Exception) {
// do nothing
}
}

return $formula;
}

private function numericOperator(DataValidation $dataValidation, int|float $cellValue, Cell $cell): bool
{
$operator = $dataValidation->getOperator();
$formula1 = $dataValidation->getFormula1();
$formula2 = $dataValidation->getFormula2();
$returnValue = false;
if ($operator === DataValidation::OPERATOR_BETWEEN) {
$returnValue = $cellValue >= $formula1 && $cellValue <= $formula2;
} elseif ($operator === DataValidation::OPERATOR_NOTBETWEEN) {
$returnValue = $cellValue < $formula1 || $cellValue > $formula2;
} elseif ($operator === DataValidation::OPERATOR_EQUAL) {
$returnValue = $cellValue == $formula1;
} elseif ($operator === DataValidation::OPERATOR_NOTEQUAL) {
$returnValue = $cellValue != $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHAN) {
$returnValue = $cellValue < $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHANOREQUAL) {
$returnValue = $cellValue <= $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHAN) {
$returnValue = $cellValue > $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHANOREQUAL) {
$returnValue = $cellValue >= $formula1;
$formula1 = self::evaluateNumericFormula(
$dataValidation->getFormula1(),
$cell
);

$formula2 = 0;
if (in_array($operator, self::TWO_FORMULAS, true)) {
$formula2 = self::evaluateNumericFormula(
$dataValidation->getFormula2(),
$cell
);
}

return $returnValue;
return match ($operator) {
DataValidation::OPERATOR_BETWEEN => $cellValue >= $formula1 && $cellValue <= $formula2,
DataValidation::OPERATOR_NOTBETWEEN => $cellValue < $formula1 || $cellValue > $formula2,
DataValidation::OPERATOR_EQUAL => $cellValue == $formula1,
DataValidation::OPERATOR_NOTEQUAL => $cellValue != $formula1,
DataValidation::OPERATOR_LESSTHAN => $cellValue < $formula1,
DataValidation::OPERATOR_LESSTHANOREQUAL => $cellValue <= $formula1,
DataValidation::OPERATOR_GREATERTHAN => $cellValue > $formula1,
DataValidation::OPERATOR_GREATERTHANOREQUAL => $cellValue >= $formula1,
default => false,
};
}

/**
Expand All @@ -94,22 +118,22 @@ private function isValueInList(Cell $cell): bool
// inline values list
if ($formula1[0] === '"') {
return in_array(strtolower($cellValueString), explode(',', strtolower(trim($formula1, '"'))), true);
} elseif (strpos($formula1, ':') > 0) {
// values list cells
$matchFormula = '=MATCH(' . $cell->getCoordinate() . ', ' . $formula1 . ', 0)';
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

try {
$result = $calculation->calculateFormula($matchFormula, $cell->getCoordinate(), $cell);
while (is_array($result)) {
$result = array_pop($result);
}
}
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

return $result !== ExcelError::NA();
} catch (Exception) {
return false;
try {
$result = $calculation->calculateFormula("=$formula1", $cell->getCoordinate(), $cell);
$result = is_array($result) ? Functions::flattenArray($result) : [$result];
foreach ($result as $oneResult) {
if (is_scalar($oneResult) && strcasecmp((string) $oneResult, $cellValueString) === 0) {
return true;
}
}
} catch (Exception) {
// do nothing
}

return false;
}

return true;
Expand Down
54 changes: 36 additions & 18 deletions src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace PhpOffice\PhpSpreadsheet\Reader\Xls;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet as XlsWorksheet;

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

foreach ($cellRangeAddresses as $cellRange) {
$stRange = $xls->phpSheet->shrinkRangeToFit($cellRange);
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
$objValidation = $xls->phpSheet->getCell($coordinate)->getDataValidation();
$objValidation->setType($type);
$objValidation->setErrorStyle($errorStyle);
$objValidation->setAllowBlank((bool) $allowBlank);
$objValidation->setShowInputMessage((bool) $showInputMessage);
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
$objValidation->setShowDropDown(!$suppressDropDown);
$objValidation->setOperator($operator);
$objValidation->setErrorTitle($errorTitle);
$objValidation->setError($error);
$objValidation->setPromptTitle($promptTitle);
$objValidation->setPrompt($prompt);
$objValidation->setFormula1($formula1);
$objValidation->setFormula2($formula2);
}
$cellRange = preg_replace(
[
"/([a-z]+)1:([a-z]+)$maxXlsRow/i",
"/([a-z]+\\d+):([a-z]+)$maxXlsRow/i",
"/A(\\d+):$maxXlsColumnString(\\d+)/i",
"/([a-z]+\\d+):$maxXlsColumnString(\\d+)/i",
],
[
'$1:$2',
'$1:${2}' . $maxRow,
'$1:$2',
'$1:' . $maxCol . '$2',
],
$cellRange
) ?? $cellRange;
$objValidation = new DataValidation();
$objValidation->setType($type);
$objValidation->setErrorStyle($errorStyle);
$objValidation->setAllowBlank((bool) $allowBlank);
$objValidation->setShowInputMessage((bool) $showInputMessage);
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
$objValidation->setShowDropDown(!$suppressDropDown);
$objValidation->setOperator($operator);
$objValidation->setErrorTitle($errorTitle);
$objValidation->setError($error);
$objValidation->setPromptTitle($promptTitle);
$objValidation->setPrompt($prompt);
$objValidation->setFormula1($formula1);
$objValidation->setFormula2($formula2);
$xls->phpSheet->setDataValidation($cellRange, $objValidation);
}
}
}
42 changes: 17 additions & 25 deletions src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
Expand Down Expand Up @@ -36,31 +37,22 @@ public function load(): void
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);
$rangeSet = explode(' ', $range);
foreach ($rangeSet as $range) {
$stRange = $this->worksheet->shrinkRangeToFit($range);

// Extract all cell references in $range
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $reference) {
// Create validation
$docValidation = $this->worksheet->getCell($reference)->getDataValidation();
$docValidation->setType((string) $dataValidation['type']);
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
$docValidation->setOperator((string) $dataValidation['operator']);
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
// showDropDown is inverted (works as hideDropDown if true)
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
$docValidation->setError((string) $dataValidation['error']);
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$docValidation->setSqref($range);
}
}
$docValidation = new DataValidation();
$docValidation->setType((string) $dataValidation['type']);
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
$docValidation->setOperator((string) $dataValidation['operator']);
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
// showDropDown is inverted (works as hideDropDown if true)
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
$docValidation->setError((string) $dataValidation['error']);
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$this->worksheet->setDataValidation($range, $docValidation);
}
}
}
Loading
Loading