Skip to content

Commit 50fa09b

Browse files
author
MarkBaker
committed
Fix issues with updating Conditional Formatting
- Update existing ranges, expanding if necessary, rather than trying to clone for each individual cell - Update conditions, so that cell references and formulae are maintained correctly Note that absolute as well as relative cell references should be updated in conditions
1 parent 78c27c0 commit 50fa09b

File tree

3 files changed

+104
-34
lines changed

3 files changed

+104
-34
lines changed

src/PhpSpreadsheet/CellReferenceHelper.php

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public function __construct(string $beforeCellAddress = 'A1', int $numberOfColum
4343
$this->beforeRow = (int) $beforeRow;
4444
}
4545

46+
public function beforeCellAddress(): string
47+
{
48+
return $this->beforeCellAddress;
49+
}
50+
4651
public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool
4752
{
4853
return $this->beforeCellAddress !== $beforeCellAddress ||

src/PhpSpreadsheet/ReferenceHelper.php

+59-34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
66
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
77
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
89
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
910
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
1011

@@ -198,6 +199,39 @@ protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows)
198199
}
199200
}
200201

202+
/**
203+
* Update conditional formatting styles when inserting/deleting rows/columns.
204+
*
205+
* @param Worksheet $worksheet The worksheet that we're editing
206+
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
207+
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
208+
*/
209+
protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void
210+
{
211+
$aStyles = $worksheet->getConditionalStylesCollection();
212+
($numberOfColumns > 0 || $numberOfRows > 0)
213+
? uksort($aStyles, [self::class, 'cellReverseSort'])
214+
: uksort($aStyles, [self::class, 'cellSort']);
215+
216+
foreach ($aStyles as $cellAddress => $cfRules) {
217+
$worksheet->removeConditionalStyles($cellAddress);
218+
$newReference = $this->updateCellReference($cellAddress);
219+
220+
foreach ($cfRules as &$cfRule) {
221+
/** @var Conditional $cfRule */
222+
$conditions = $cfRule->getConditions();
223+
foreach ($conditions as &$condition) {
224+
if (is_string($condition)) {
225+
$condition = $this->updateFormulaReferences(
226+
$condition, $this->cellReferenceHelper->beforeCellAddress(), $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
227+
}
228+
}
229+
$cfRule->setConditions($conditions);
230+
}
231+
$worksheet->setConditionalStyles($newReference, $cfRules);
232+
}
233+
}
234+
201235
/**
202236
* Update data validations when inserting/deleting rows/columns.
203237
*
@@ -442,6 +476,9 @@ function ($coordinate) use ($allCoordinates) {
442476
// Update worksheet: hyperlinks
443477
$this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);
444478

479+
// Update worksheet: conditional formatting styles
480+
$this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);
481+
445482
// Update worksheet: data validations
446483
$this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows);
447484

@@ -505,8 +542,14 @@ function ($coordinate) use ($allCoordinates) {
505542
*
506543
* @return string Updated formula
507544
*/
508-
public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '')
509-
{
545+
public function updateFormulaReferences(
546+
$formula = '',
547+
$beforeCellAddress = 'A1',
548+
$numberOfColumns = 0,
549+
$numberOfRows = 0,
550+
$worksheetName = '',
551+
bool $includeAbsoluteReferences = false
552+
) {
510553
if (
511554
$this->cellReferenceHelper === null ||
512555
$this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
@@ -528,8 +571,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
528571
foreach ($matches as $match) {
529572
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
530573
$fromString .= $match[3] . ':' . $match[4];
531-
$modified3 = substr($this->updateCellReference('$A' . $match[3]), 2);
532-
$modified4 = substr($this->updateCellReference('$A' . $match[4]), 2);
574+
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2);
575+
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2);
533576

534577
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
535578
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -553,8 +596,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
553596
foreach ($matches as $match) {
554597
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
555598
$fromString .= $match[3] . ':' . $match[4];
556-
$modified3 = substr($this->updateCellReference($match[3] . '$1'), 0, -2);
557-
$modified4 = substr($this->updateCellReference($match[4] . '$1'), 0, -2);
599+
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2);
600+
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2);
558601

559602
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
560603
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -578,8 +621,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
578621
foreach ($matches as $match) {
579622
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
580623
$fromString .= $match[3] . ':' . $match[4];
581-
$modified3 = $this->updateCellReference($match[3]);
582-
$modified4 = $this->updateCellReference($match[4]);
624+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
625+
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences);
583626

584627
if ($match[3] . $match[4] !== $modified3 . $modified4) {
585628
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -606,7 +649,7 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
606649
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
607650
$fromString .= $match[3];
608651

609-
$modified3 = $this->updateCellReference($match[3]);
652+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
610653
if ($match[3] !== $modified3) {
611654
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
612655
$toString = ($match[2] > '') ? $match[2] . '!' : '';
@@ -786,18 +829,18 @@ private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows
786829
*
787830
* @return string Updated cell range
788831
*/
789-
private function updateCellReference($cellReference = 'A1')
832+
private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false)
790833
{
791834
// Is it in another worksheet? Will not have to update anything.
792835
if (strpos($cellReference, '!') !== false) {
793836
return $cellReference;
794837
// Is it a range or a single cell?
795838
} elseif (!Coordinate::coordinateIsRange($cellReference)) {
796839
// Single cell
797-
return $this->cellReferenceHelper->updateCellReference($cellReference);
840+
return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences);
798841
} elseif (Coordinate::coordinateIsRange($cellReference)) {
799842
// Range
800-
return $this->updateCellRange($cellReference);
843+
return $this->updateCellRange($cellReference, $includeAbsoluteReferences);
801844
}
802845

803846
// Return original
@@ -839,7 +882,7 @@ public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $ne
839882
*
840883
* @return string Updated cell range
841884
*/
842-
private function updateCellRange(string $cellRange = 'A1:A1'): string
885+
private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string
843886
{
844887
if (!Coordinate::coordinateIsRange($cellRange)) {
845888
throw new Exception('Only cell ranges may be passed to this method.');
@@ -853,14 +896,14 @@ private function updateCellRange(string $cellRange = 'A1:A1'): string
853896
for ($j = 0; $j < $jc; ++$j) {
854897
if (ctype_alpha($range[$i][$j])) {
855898
$range[$i][$j] = Coordinate::coordinateFromString(
856-
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1')
899+
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences)
857900
)[0];
858901
} elseif (ctype_digit($range[$i][$j])) {
859902
$range[$i][$j] = Coordinate::coordinateFromString(
860-
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j])
903+
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences)
861904
)[1];
862905
} else {
863-
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j]);
906+
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences);
864907
}
865908
}
866909
}
@@ -985,17 +1028,8 @@ private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn
9851028
$coordinate = $beforeColumnName . $i;
9861029
if ($worksheet->cellExists($coordinate)) {
9871030
$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
988-
$conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ?
989-
$worksheet->getConditionalStyles($coordinate) : false;
9901031
for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
9911032
$worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex);
992-
if ($conditionalStyles) {
993-
$cloned = [];
994-
foreach ($conditionalStyles as $conditionalStyle) {
995-
$cloned[] = clone $conditionalStyle;
996-
}
997-
$worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned);
998-
}
9991033
}
10001034
}
10011035
}
@@ -1009,17 +1043,8 @@ private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, i
10091043
$coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
10101044
if ($worksheet->cellExists($coordinate)) {
10111045
$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
1012-
$conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ?
1013-
$worksheet->getConditionalStyles($coordinate) : false;
10141046
for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
10151047
$worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
1016-
if ($conditionalStyles) {
1017-
$cloned = [];
1018-
foreach ($conditionalStyles as $conditionalStyle) {
1019-
$cloned[] = clone $conditionalStyle;
1020-
}
1021-
$worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned);
1022-
}
10231048
}
10241049
}
10251050
}

tests/PhpSpreadsheetTests/ReferenceHelperTest.php

+40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Comment;
88
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
99
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
1011
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
1112
use PHPUnit\Framework\TestCase;
1213

@@ -296,4 +297,43 @@ function (Hyperlink $value) {
296297

297298
self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks);
298299
}
300+
301+
public function testInsertRowsWithConditionalFormatting(): void
302+
{
303+
$spreadsheet = new Spreadsheet();
304+
$sheet = $spreadsheet->getActiveSheet();
305+
$sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true);
306+
$sheet->getCell('H5')->setValue(5);
307+
308+
$cellRange = 'C3:F7';
309+
$conditionalStyles = [];
310+
$wizardFactory = new Wizard($cellRange);
311+
/** @var Wizard\CellValue $cellWizard */
312+
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);
313+
314+
$cellWizard->equals('$H$5', Wizard::VALUE_TYPE_CELL);
315+
$conditionalStyles[] = $cellWizard->getConditional();
316+
317+
$cellWizard->greaterThan('$H$5', Wizard::VALUE_TYPE_CELL);
318+
$conditionalStyles[] = $cellWizard->getConditional();
319+
320+
$cellWizard->lessThan('$H$5', Wizard::VALUE_TYPE_CELL);
321+
$conditionalStyles[] = $cellWizard->getConditional();
322+
323+
$spreadsheet->getActiveSheet()
324+
->getStyle($cellWizard->getCellRange())
325+
->setConditionalStyles($conditionalStyles);
326+
$sheet->insertNewRowBefore(4, 2);
327+
328+
$styles = $sheet->getConditionalStylesCollection();
329+
// verify that the conditional range has been updated
330+
self::assertSame('C3:F9', array_keys($styles)[0]);
331+
// verify that the conditions have been updated
332+
foreach ($styles as $style) {
333+
foreach ($style as $conditions) {
334+
self::assertSame('$H$7', $conditions->getConditions()[0]);
335+
}
336+
}
337+
}
338+
299339
}

0 commit comments

Comments
 (0)