diff --git a/docs/topics/autofilters.md b/docs/topics/autofilters.md index d8f37b60..a8028369 100644 --- a/docs/topics/autofilters.md +++ b/docs/topics/autofilters.md @@ -65,6 +65,13 @@ $spreadsheet->getActiveSheet()->setAutoFilter( This enables filtering, but does not actually apply any filters. +After setting the range, you can change it so that the end row is the +last row used on the worksheet: + +```php +$spreadsheet->getActiveSheet()->getAutoFilter()->setRangeToMaxRow(); +``` + ## Autofilter Expressions PHPEXcel 1.7.8 introduced the ability to actually create, read and write @@ -503,6 +510,23 @@ $autoFilter->showHideRows(); This will set all rows that match the filter criteria to visible, while hiding all other rows within the autofilter area. +Excel allows you to explicitly hide a row after applying a filter even +if the row wasn't hidden by the filter. However, if a row is hidden *before* +applying the filter, and the filter is applied, the row will no longer be hidden. +This can make a difference during PhpSpreadsheet save, since PhpSpreadsheet +will apply the filter during save if it hasn't been previously applied, +or if the filter criteria have changed since it was last applied. +Note that an autofilter read in from an existing spreadsheet is assumed to have been applied. +Also note that changing the data in the columns being filtered +does not result in reevaluation in either Excel or PhpSpreadsheet. +If you wish to re-apply all filters in the spreadsheet +(possibly just before save): +```php +$spreadsheet->reevaluateAutoFilters(false); +``` +You can specify `true` rather than `false` to adjust the filter ranges +on each sheet so that they end at the last row used on the sheet. + ### Displaying Filtered Rows Simply looping through the rows in an autofilter area will still access diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 155ed731..b88f9056 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -61,6 +61,7 @@ class AutoFilter // Check for dynamic filters $this->readTopTenAutoFilter($filterColumn, $column); } + $autoFilter->setEvaluated(true); } private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $column): void diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 9990a68d..350ba652 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -1589,4 +1589,17 @@ class Spreadsheet throw new Exception('Tab ratio must be between 0 and 1000.'); } } + + public function reevaluateAutoFilters(bool $resetToMax): void + { + foreach ($this->workSheetCollection as $sheet) { + $filter = $sheet->getAutoFilter(); + if (!empty($filter->getRange())) { + if ($resetToMax) { + $filter->setRangeToMaxRow(); + } + $filter->showHideRows(); + } + } + } } diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 01dd8374..e7d633fe 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -35,6 +35,19 @@ class AutoFilter */ private $columns = []; + /** @var bool */ + private $evaluated = false; + + public function getEvaluated(): bool + { + return $this->evaluated; + } + + public function setEvaluated(bool $value): void + { + $this->evaluated = $value; + } + /** * Create a new AutoFilter. * @@ -63,6 +76,7 @@ class AutoFilter */ public function setParent(?Worksheet $worksheet = null) { + $this->evaluated = false; $this->workSheet = $worksheet; return $this; @@ -87,6 +101,7 @@ class AutoFilter */ public function setRange($range) { + $this->evaluated = false; // extract coordinate [$worksheet, $range] = Worksheet::extractSheetTitle($range, true); if (empty($range)) { @@ -114,6 +129,20 @@ class AutoFilter return $this; } + public function setRangeToMaxRow(): self + { + $this->evaluated = false; + if ($this->workSheet !== null) { + $thisrange = $this->range; + $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + if ($range !== $thisrange) { + $this->setRange($range); + } + } + + return $this; + } + /** * Get all AutoFilter Columns. * @@ -201,6 +230,7 @@ class AutoFilter */ public function setColumn($columnObjectOrString) { + $this->evaluated = false; if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) { $column = $columnObjectOrString; } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) { @@ -230,6 +260,7 @@ class AutoFilter */ public function clearColumn($column) { + $this->evaluated = false; $this->testColumnInRange($column); if (isset($this->columns[$column])) { @@ -253,6 +284,7 @@ class AutoFilter */ public function shiftColumn($fromColumn, $toColumn) { + $this->evaluated = false; $fromColumn = strtoupper($fromColumn); $toColumn = strtoupper($toColumn); @@ -1002,6 +1034,7 @@ class AutoFilter // Set show/hide for the row based on the result of the autoFilter result $this->workSheet->getRowDimension((int) $row)->setVisible($result); } + $this->evaluated = true; return $this; } diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php index ef741ee3..2e3ea65b 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -100,6 +100,13 @@ class Column $this->parent = $parent; } + public function setEvaluatedFalse(): void + { + if ($this->parent !== null) { + $this->parent->setEvaluated(false); + } + } + /** * Get AutoFilter column index as string eg: 'A'. * @@ -119,6 +126,7 @@ class Column */ public function setColumnIndex($column) { + $this->setEvaluatedFalse(); // Uppercase coordinate $column = strtoupper($column); if ($this->parent !== null) { @@ -147,6 +155,7 @@ class Column */ public function setParent(?AutoFilter $parent = null) { + $this->setEvaluatedFalse(); $this->parent = $parent; return $this; @@ -171,6 +180,7 @@ class Column */ public function setFilterType($filterType) { + $this->setEvaluatedFalse(); if (!in_array($filterType, self::$filterTypes)) { throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.'); } @@ -202,6 +212,7 @@ class Column */ public function setJoin($join) { + $this->setEvaluatedFalse(); // Lowercase And/Or $join = strtolower($join); if (!in_array($join, self::$ruleJoins)) { @@ -222,6 +233,7 @@ class Column */ public function setAttributes($attributes) { + $this->setEvaluatedFalse(); $this->attributes = $attributes; return $this; @@ -237,6 +249,7 @@ class Column */ public function setAttribute($name, $value) { + $this->setEvaluatedFalse(); $this->attributes[$name] = $value; return $this; @@ -306,6 +319,7 @@ class Column */ public function createRule() { + $this->setEvaluatedFalse(); if ($this->filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) >= 2) { throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter'); } @@ -321,6 +335,7 @@ class Column */ public function addRule(Column\Rule $rule) { + $this->setEvaluatedFalse(); $rule->setParent($this); $this->ruleset[] = $rule; @@ -337,6 +352,7 @@ class Column */ public function deleteRule($index) { + $this->setEvaluatedFalse(); if (isset($this->ruleset[$index])) { unset($this->ruleset[$index]); // If we've just deleted down to a single rule, then reset And/Or joining to Or @@ -355,6 +371,7 @@ class Column */ public function clearRules() { + $this->setEvaluatedFalse(); $this->ruleset = []; $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR); diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php index 525b06f6..408dfb3f 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php @@ -213,6 +213,13 @@ class Rule $this->parent = $parent; } + private function setEvaluatedFalse(): void + { + if ($this->parent !== null) { + $this->parent->setEvaluatedFalse(); + } + } + /** * Get AutoFilter Rule Type. * @@ -232,6 +239,7 @@ class Rule */ public function setRuleType($ruleType) { + $this->setEvaluatedFalse(); if (!in_array($ruleType, self::RULE_TYPES)) { throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.'); } @@ -260,6 +268,7 @@ class Rule */ public function setValue($value) { + $this->setEvaluatedFalse(); if (is_array($value)) { $grouping = -1; foreach ($value as $key => $v) { @@ -302,6 +311,7 @@ class Rule */ public function setOperator($operator) { + $this->setEvaluatedFalse(); if (empty($operator)) { $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL; } @@ -335,6 +345,7 @@ class Rule */ public function setGrouping($grouping) { + $this->setEvaluatedFalse(); if ( ($grouping !== null) && (!in_array($grouping, self::DATE_TIME_GROUPS)) && @@ -359,6 +370,7 @@ class Rule */ public function setRule($operator, $value, $grouping = null) { + $this->setEvaluatedFalse(); $this->setOperator($operator); $this->setValue($value); // Only set grouping if it's been passed in as a user-supplied argument, @@ -388,6 +400,7 @@ class Rule */ public function setParent(?Column $parent = null) { + $this->setEvaluatedFalse(); $this->parent = $parent; return $this; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 3e1b537e..494dc70a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -145,7 +145,9 @@ class Worksheet extends WriterPart $autoFilterRange = $worksheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { $objWriter->writeAttribute('filterMode', 1); - $worksheet->getAutoFilter()->showHideRows(); + if (!$worksheet->getAutoFilter()->getEvaluated()) { + $worksheet->getAutoFilter()->showHideRows(); + } } // tabColor diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilterEvaluateTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilterEvaluateTest.php new file mode 100644 index 00000000..cb54c567 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilterEvaluateTest.php @@ -0,0 +1,149 @@ +getHighestRow(); ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + + return $actualVisible; + } + + private function initializeSpreadsheet(): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Filtered'); + for ($row = 2; $row <= 15; ++$row) { + $sheet->getCell("A$row")->setValue($row % 5); + } + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange('A1:A15'); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + $columnFilter->createRule()->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, 1); + $columnFilter->createRule()->setRule(Rule::AUTOFILTER_COLUMN_RULE_EQUAL, 3); + $sheet->getCell('A16')->setValue(2); // outside of range + + return $spreadsheet; + } + + public function testAutoFilterUnevaluated(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + //$sheet->getAutoFilter->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + //$columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + // Because showHideRows isn't executed beforehand, it will be executed + // at write time, causing row 6 to be visible. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 6, 8, 11, 13, 16], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testAutoFilterEvaluated(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getAutoFilter()->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + //$columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + // Because showHideRows is executed beforehand, it won't be executed + // at write time, so row 6 will remain hidden. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 8, 11, 13, 16], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testAutoFilterReevaluated(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getAutoFilter()->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + $sheet->getAutoFilter()->getColumn('A')->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + // Because filter is changed after showHideRows, it will be reevaluated + // at write time, so row 6 will be visible. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 6, 8, 11, 13, 16], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testAutoFilterSetRangeMax(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Filtered'); + $sheet->getAutoFilter()->setRangeToMaxRow(); + $sheet->getAutoFilter()->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + $sheet->getAutoFilter()->getColumn('A')->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + // Because filter is changed after showHideRows, it will be reevaluated + // at write time, so row 6 will be visible. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 6, 8, 11, 13], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testAutoFilterSpreadsheetReevaluate(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Filtered'); + $sheet->getAutoFilter()->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + $spreadsheet->reevaluateAutoFilters(false); + // Because filter is changed after showHideRows, it will be reevaluated + // at write time, so row 6 will be visible. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 6, 8, 11, 13, 16], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testAutoFilterSpreadsheetReevaluateResetMax(): void + { + $spreadsheet = $this->initializeSpreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Filtered'); + $sheet->getAutoFilter()->showHideRows(); + $sheet->getRowDimension(6)->setVisible(false); + $spreadsheet->reevaluateAutoFilters(true); + // Because filter is changed after showHideRows, it will be reevaluated + // at write time, so row 6 will be visible. + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([3, 6, 8, 11, 13], $this->getVisibleSheet($rsheet)); + $reloadedSpreadsheet->disconnectWorksheets(); + } +}