Skip to content

Commit 9c8eeef

Browse files
authored
Xlsx Writer Unhides Explicitly Hidden Row in Filter Range - Minor Breaking Change (#2414)
Fix #1641. Excel allows explicit hiding of row after filter is applied, but PhpSpreadsheet automatically invokes showHideRows on all auto-filters, preventing users from doing the same. Change to invoke showHideRows only if it hasn't already been invoked, or if filter criteria have changed since it was last invoked. Autofilters read in from an existing spreadsheet are assumed to be already invoked. This is potentially a breaking change, probably a minor one. The conditions to set up 1641 are probably uncommon, but users who meet those conditions and are happy with the current behavior will see a break. The new behavior is closer to how Excel itself behaves. A new method `reevaluateAutoFilters` is added to `Spreadsheet`; this can be used to restore the old behavior if desired. The new method is added to the documentation, along with a description of how the situation described in 1641 is handled in Excel and PhpSpreadsheet. While examining Excel's behavior, it became evident that, although a filter is applied to an entire column, it is actually applied only to the rows that are populated when the filter is defined, as can be verified by examining the XML definition of the filter. When you re-apply the filter, rows that have been added since are considered. It would be useful to provide PhpSpreadsheet with a method to do the same. I have added, and documented, `setRangeToMaxRow` to `AutoFilter`.
1 parent aa91abc commit 9c8eeef

File tree

8 files changed

+253
-1
lines changed

8 files changed

+253
-1
lines changed

docs/topics/autofilters.md

+24
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ $spreadsheet->getActiveSheet()->setAutoFilter(
6565

6666
This enables filtering, but does not actually apply any filters.
6767

68+
After setting the range, you can change it so that the end row is the
69+
last row used on the worksheet:
70+
71+
```php
72+
$spreadsheet->getActiveSheet()->getAutoFilter()->setRangeToMaxRow();
73+
```
74+
6875
## Autofilter Expressions
6976

7077
PHPEXcel 1.7.8 introduced the ability to actually create, read and write
@@ -503,6 +510,23 @@ $autoFilter->showHideRows();
503510
This will set all rows that match the filter criteria to visible, while
504511
hiding all other rows within the autofilter area.
505512

513+
Excel allows you to explicitly hide a row after applying a filter even
514+
if the row wasn't hidden by the filter. However, if a row is hidden *before*
515+
applying the filter, and the filter is applied, the row will no longer be hidden.
516+
This can make a difference during PhpSpreadsheet save, since PhpSpreadsheet
517+
will apply the filter during save if it hasn't been previously applied,
518+
or if the filter criteria have changed since it was last applied.
519+
Note that an autofilter read in from an existing spreadsheet is assumed to have been applied.
520+
Also note that changing the data in the columns being filtered
521+
does not result in reevaluation in either Excel or PhpSpreadsheet.
522+
If you wish to re-apply all filters in the spreadsheet
523+
(possibly just before save):
524+
```php
525+
$spreadsheet->reevaluateAutoFilters(false);
526+
```
527+
You can specify `true` rather than `false` to adjust the filter ranges
528+
on each sheet so that they end at the last row used on the sheet.
529+
506530
### Displaying Filtered Rows
507531

508532
Simply looping through the rows in an autofilter area will still access

src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private function readAutoFilter($autoFilterRange, $xmlSheet): void
6161
// Check for dynamic filters
6262
$this->readTopTenAutoFilter($filterColumn, $column);
6363
}
64+
$autoFilter->setEvaluated(true);
6465
}
6566

6667
private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $column): void

src/PhpSpreadsheet/Spreadsheet.php

+13
Original file line numberDiff line numberDiff line change
@@ -1589,4 +1589,17 @@ public function setTabRatio($tabRatio): void
15891589
throw new Exception('Tab ratio must be between 0 and 1000.');
15901590
}
15911591
}
1592+
1593+
public function reevaluateAutoFilters(bool $resetToMax): void
1594+
{
1595+
foreach ($this->workSheetCollection as $sheet) {
1596+
$filter = $sheet->getAutoFilter();
1597+
if (!empty($filter->getRange())) {
1598+
if ($resetToMax) {
1599+
$filter->setRangeToMaxRow();
1600+
}
1601+
$filter->showHideRows();
1602+
}
1603+
}
1604+
}
15921605
}

src/PhpSpreadsheet/Worksheet/AutoFilter.php

+33
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ class AutoFilter
3535
*/
3636
private $columns = [];
3737

38+
/** @var bool */
39+
private $evaluated = false;
40+
41+
public function getEvaluated(): bool
42+
{
43+
return $this->evaluated;
44+
}
45+
46+
public function setEvaluated(bool $value): void
47+
{
48+
$this->evaluated = $value;
49+
}
50+
3851
/**
3952
* Create a new AutoFilter.
4053
*
@@ -63,6 +76,7 @@ public function getParent()
6376
*/
6477
public function setParent(?Worksheet $worksheet = null)
6578
{
79+
$this->evaluated = false;
6680
$this->workSheet = $worksheet;
6781

6882
return $this;
@@ -87,6 +101,7 @@ public function getRange()
87101
*/
88102
public function setRange($range)
89103
{
104+
$this->evaluated = false;
90105
// extract coordinate
91106
[$worksheet, $range] = Worksheet::extractSheetTitle($range, true);
92107
if (empty($range)) {
@@ -114,6 +129,20 @@ public function setRange($range)
114129
return $this;
115130
}
116131

132+
public function setRangeToMaxRow(): self
133+
{
134+
$this->evaluated = false;
135+
if ($this->workSheet !== null) {
136+
$thisrange = $this->range;
137+
$range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? '';
138+
if ($range !== $thisrange) {
139+
$this->setRange($range);
140+
}
141+
}
142+
143+
return $this;
144+
}
145+
117146
/**
118147
* Get all AutoFilter Columns.
119148
*
@@ -201,6 +230,7 @@ public function getColumnByOffset($columnOffset)
201230
*/
202231
public function setColumn($columnObjectOrString)
203232
{
233+
$this->evaluated = false;
204234
if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
205235
$column = $columnObjectOrString;
206236
} elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) {
@@ -230,6 +260,7 @@ public function setColumn($columnObjectOrString)
230260
*/
231261
public function clearColumn($column)
232262
{
263+
$this->evaluated = false;
233264
$this->testColumnInRange($column);
234265

235266
if (isset($this->columns[$column])) {
@@ -253,6 +284,7 @@ public function clearColumn($column)
253284
*/
254285
public function shiftColumn($fromColumn, $toColumn)
255286
{
287+
$this->evaluated = false;
256288
$fromColumn = strtoupper($fromColumn);
257289
$toColumn = strtoupper($toColumn);
258290

@@ -1002,6 +1034,7 @@ public function showHideRows()
10021034
// Set show/hide for the row based on the result of the autoFilter result
10031035
$this->workSheet->getRowDimension((int) $row)->setVisible($result);
10041036
}
1037+
$this->evaluated = true;
10051038

10061039
return $this;
10071040
}

src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php

+17
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ public function __construct($column, ?AutoFilter $parent = null)
100100
$this->parent = $parent;
101101
}
102102

103+
public function setEvaluatedFalse(): void
104+
{
105+
if ($this->parent !== null) {
106+
$this->parent->setEvaluated(false);
107+
}
108+
}
109+
103110
/**
104111
* Get AutoFilter column index as string eg: 'A'.
105112
*
@@ -119,6 +126,7 @@ public function getColumnIndex()
119126
*/
120127
public function setColumnIndex($column)
121128
{
129+
$this->setEvaluatedFalse();
122130
// Uppercase coordinate
123131
$column = strtoupper($column);
124132
if ($this->parent !== null) {
@@ -147,6 +155,7 @@ public function getParent()
147155
*/
148156
public function setParent(?AutoFilter $parent = null)
149157
{
158+
$this->setEvaluatedFalse();
150159
$this->parent = $parent;
151160

152161
return $this;
@@ -171,6 +180,7 @@ public function getFilterType()
171180
*/
172181
public function setFilterType($filterType)
173182
{
183+
$this->setEvaluatedFalse();
174184
if (!in_array($filterType, self::$filterTypes)) {
175185
throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.');
176186
}
@@ -202,6 +212,7 @@ public function getJoin()
202212
*/
203213
public function setJoin($join)
204214
{
215+
$this->setEvaluatedFalse();
205216
// Lowercase And/Or
206217
$join = strtolower($join);
207218
if (!in_array($join, self::$ruleJoins)) {
@@ -222,6 +233,7 @@ public function setJoin($join)
222233
*/
223234
public function setAttributes($attributes)
224235
{
236+
$this->setEvaluatedFalse();
225237
$this->attributes = $attributes;
226238

227239
return $this;
@@ -237,6 +249,7 @@ public function setAttributes($attributes)
237249
*/
238250
public function setAttribute($name, $value)
239251
{
252+
$this->setEvaluatedFalse();
240253
$this->attributes[$name] = $value;
241254

242255
return $this;
@@ -306,6 +319,7 @@ public function getRule($index)
306319
*/
307320
public function createRule()
308321
{
322+
$this->setEvaluatedFalse();
309323
if ($this->filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) >= 2) {
310324
throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter');
311325
}
@@ -321,6 +335,7 @@ public function createRule()
321335
*/
322336
public function addRule(Column\Rule $rule)
323337
{
338+
$this->setEvaluatedFalse();
324339
$rule->setParent($this);
325340
$this->ruleset[] = $rule;
326341

@@ -337,6 +352,7 @@ public function addRule(Column\Rule $rule)
337352
*/
338353
public function deleteRule($index)
339354
{
355+
$this->setEvaluatedFalse();
340356
if (isset($this->ruleset[$index])) {
341357
unset($this->ruleset[$index]);
342358
// If we've just deleted down to a single rule, then reset And/Or joining to Or
@@ -355,6 +371,7 @@ public function deleteRule($index)
355371
*/
356372
public function clearRules()
357373
{
374+
$this->setEvaluatedFalse();
358375
$this->ruleset = [];
359376
$this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
360377

src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php

+13
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ public function __construct(?Column $parent = null)
213213
$this->parent = $parent;
214214
}
215215

216+
private function setEvaluatedFalse(): void
217+
{
218+
if ($this->parent !== null) {
219+
$this->parent->setEvaluatedFalse();
220+
}
221+
}
222+
216223
/**
217224
* Get AutoFilter Rule Type.
218225
*
@@ -232,6 +239,7 @@ public function getRuleType()
232239
*/
233240
public function setRuleType($ruleType)
234241
{
242+
$this->setEvaluatedFalse();
235243
if (!in_array($ruleType, self::RULE_TYPES)) {
236244
throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
237245
}
@@ -260,6 +268,7 @@ public function getValue()
260268
*/
261269
public function setValue($value)
262270
{
271+
$this->setEvaluatedFalse();
263272
if (is_array($value)) {
264273
$grouping = -1;
265274
foreach ($value as $key => $v) {
@@ -302,6 +311,7 @@ public function getOperator()
302311
*/
303312
public function setOperator($operator)
304313
{
314+
$this->setEvaluatedFalse();
305315
if (empty($operator)) {
306316
$operator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
307317
}
@@ -335,6 +345,7 @@ public function getGrouping()
335345
*/
336346
public function setGrouping($grouping)
337347
{
348+
$this->setEvaluatedFalse();
338349
if (
339350
($grouping !== null) &&
340351
(!in_array($grouping, self::DATE_TIME_GROUPS)) &&
@@ -359,6 +370,7 @@ public function setGrouping($grouping)
359370
*/
360371
public function setRule($operator, $value, $grouping = null)
361372
{
373+
$this->setEvaluatedFalse();
362374
$this->setOperator($operator);
363375
$this->setValue($value);
364376
// Only set grouping if it's been passed in as a user-supplied argument,
@@ -388,6 +400,7 @@ public function getParent()
388400
*/
389401
public function setParent(?Column $parent = null)
390402
{
403+
$this->setEvaluatedFalse();
391404
$this->parent = $parent;
392405

393406
return $this;

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $wor
145145
$autoFilterRange = $worksheet->getAutoFilter()->getRange();
146146
if (!empty($autoFilterRange)) {
147147
$objWriter->writeAttribute('filterMode', 1);
148-
$worksheet->getAutoFilter()->showHideRows();
148+
if (!$worksheet->getAutoFilter()->getEvaluated()) {
149+
$worksheet->getAutoFilter()->showHideRows();
150+
}
149151
}
150152

151153
// tabColor

0 commit comments

Comments
 (0)