Skip to content

Commit 56e7422

Browse files
committed
Add "Priority" Property for Conditional Formatting
Fix PHPOffice#4311. Excel applies Conditional Formatting rules according to a priority specified in the xml. The priority must be a natural number; the rules are applied in order from lowest priority number to highest. When reading an Xlsx spreadsheet, PhpSpreadsheet has been ignoring the priority, which can result in differences from Excel's behavior, especially when CF cell ranges overlap (note that overlapping ranges are not supported in Xls format). If an application uses PhpSpreadsheet to add new Conditional Formatting to a worksheet and does not change its priority from the default (0), the Xlsx Writer will assign a priority with a higher value than any of the CF objects which have been assigned a priority (either from reading it or explicitly assigning it).
1 parent fb757cf commit 56e7422

File tree

6 files changed

+157
-5
lines changed

6 files changed

+157
-5
lines changed

src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX
125125
{
126126
$conditionType = (string) $attributes->type;
127127
$operatorType = (string) $attributes->operator;
128+
$priority = (int) (string) $attributes->priority;
128129

129130
$operands = [];
130131
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
@@ -134,6 +135,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX
134135
$conditional = new Conditional();
135136
$conditional->setConditionType($conditionType);
136137
$conditional->setOperatorType($operatorType);
138+
$conditional->setPriority($priority);
137139
if (
138140
$conditionType === Conditional::CONDITION_CONTAINSTEXT
139141
|| $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT
@@ -184,7 +186,7 @@ private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
184186
private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void
185187
{
186188
foreach ($conditionals as $cellRangeReference => $cfRules) {
187-
ksort($cfRules);
189+
ksort($cfRules); // no longer needed for Xlsx, but helps Xls
188190
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
189191

190192
// Extract all cell references in $cellRangeReference
@@ -205,6 +207,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
205207
$objConditional = new Conditional();
206208
$objConditional->setConditionType((string) $cfRule['type']);
207209
$objConditional->setOperatorType((string) $cfRule['operator']);
210+
$objConditional->setPriority((int) (string) $cfRule['priority']);
208211
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));
209212

210213
if ((string) $cfRule['text'] != '') {

src/PhpSpreadsheet/Style/Conditional.php

+14
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Conditional implements IComparable
106106

107107
private bool $noFormatSet = false;
108108

109+
private int $priority = 0;
110+
109111
/**
110112
* Create a new Conditional.
111113
*/
@@ -115,6 +117,18 @@ public function __construct()
115117
$this->style = new Style(false, true);
116118
}
117119

120+
public function getPriority(): int
121+
{
122+
return $this->priority;
123+
}
124+
125+
public function setPriority(int $priority): self
126+
{
127+
$this->priority = $priority;
128+
129+
return $this;
130+
}
131+
118132
public function getNoFormatSet(): bool
119133
{
120134
return $this->noFormatSet;

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,12 @@ private static function writeColorScaleElements(XMLWriter $objWriter, ?Condition
861861
private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
862862
{
863863
// Conditional id
864-
$id = 1;
864+
$id = 0;
865+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
866+
foreach ($conditionalStyles as $conditional) {
867+
$id = max($id, $conditional->getPriority());
868+
}
869+
}
865870

866871
// Loop through styles in the current worksheet
867872
foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
@@ -888,7 +893,8 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
888893
'dxfId',
889894
(string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
890895
);
891-
$objWriter->writeAttribute('priority', (string) $id++);
896+
$priority = $conditional->getPriority() ?: ++$id;
897+
$objWriter->writeAttribute('priority', (string) $priority);
892898

893899
self::writeAttributeif(
894900
$objWriter,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\IOFactory;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
10+
use PhpOffice\PhpSpreadsheet\Style\Fill;
11+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
12+
13+
class ConditionalPriorityTest extends AbstractFunctional
14+
{
15+
public function testConditionalPriority(): void
16+
{
17+
$filename = 'tests/data/Reader/XLSX/issue.4312c.xlsx';
18+
$reader = IOFactory::createReader('Xlsx');
19+
$spreadsheet = $reader->load($filename);
20+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
21+
$spreadsheet->disconnectWorksheets();
22+
$worksheet = $reloadedSpreadsheet->getActiveSheet();
23+
$priorities = [];
24+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
25+
foreach ($conditionalStyles as $conditional) {
26+
$priorities[] = $conditional->getPriority();
27+
}
28+
}
29+
$expected = [27, 2, 3, 4, 1, 22, 14, 5, 6, 7, 20];
30+
self::assertSame($expected, $priorities);
31+
$reloadedSpreadsheet->disconnectWorksheets();
32+
}
33+
34+
public function testZeroPriority(): void
35+
{
36+
$spreadsheet = new Spreadsheet();
37+
$sheet = $spreadsheet->getActiveSheet();
38+
$sheet->fromArray([
39+
[1, 1, 1, 1],
40+
[2, 2, 2, 2],
41+
[3, 3, 3, 3],
42+
[4, 4, 4, 4],
43+
[5, 5, 5, 5],
44+
]);
45+
46+
$range = 'A1:A5';
47+
$styles = [];
48+
$new = new Conditional();
49+
$new->setConditionType(Conditional::CONDITION_CELLIS)
50+
->setOperatorType(Conditional::OPERATOR_EQUAL)
51+
->setPriority(30)
52+
->setConditions(['3'])
53+
->getStyle()
54+
->getFill()
55+
->setFillType(Fill::FILL_SOLID)
56+
->getStartColor()
57+
->setArgb('FFC00000');
58+
$styles[] = $new;
59+
$sheet->setConditionalStyles($range, $styles);
60+
61+
$range = 'B1:B5';
62+
$styles = [];
63+
$new = new Conditional();
64+
$new->setConditionType(Conditional::CONDITION_EXPRESSION)
65+
->setConditions('=MOD(A1,2)=0')
66+
->getStyle()
67+
->getFill()
68+
->setFillType(Fill::FILL_SOLID)
69+
->getStartColor()
70+
->setArgb('FF00B0F0');
71+
$styles[] = $new;
72+
$new = new Conditional();
73+
$new->setConditionType(Conditional::CONDITION_CELLIS)
74+
->setOperatorType(Conditional::OPERATOR_EQUAL)
75+
->setPriority(40)
76+
->setConditions(['4'])
77+
->getStyle()
78+
->getFill()
79+
->setFillType(Fill::FILL_SOLID)
80+
->getStartColor()
81+
->setArgb('FFFFC000');
82+
$styles[] = $new;
83+
$sheet->setConditionalStyles($range, $styles);
84+
85+
$range = 'C1:C5';
86+
$styles = [];
87+
$new = new Conditional();
88+
$new->setConditionType(Conditional::CONDITION_CELLIS)
89+
->setOperatorType(Conditional::OPERATOR_EQUAL)
90+
->setPriority(20)
91+
->setConditions(['2'])
92+
->getStyle()
93+
->getFill()
94+
->setFillType(Fill::FILL_SOLID)
95+
->getStartColor()
96+
->setArgb('FFFFFF00');
97+
$styles[] = $new;
98+
$new = new Conditional();
99+
$new->setConditionType(Conditional::CONDITION_CELLIS)
100+
->setOperatorType(Conditional::OPERATOR_EQUAL)
101+
->setConditions(['5'])
102+
->getStyle()
103+
->getFill()
104+
->setFillType(Fill::FILL_SOLID)
105+
->getStartColor()
106+
->setArgb('FF008080');
107+
$styles[] = $new;
108+
$sheet->setConditionalStyles($range, $styles);
109+
110+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
111+
$spreadsheet->disconnectWorksheets();
112+
$worksheet = $reloadedSpreadsheet->getActiveSheet();
113+
$priorities = [];
114+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
115+
foreach ($conditionalStyles as $conditional) {
116+
$priorities[] = $conditional->getPriority();
117+
}
118+
}
119+
// B1:B5 is written in order 41, 40, but Reader sorts them
120+
$expected = [30, 40, 41, 20, 42];
121+
self::assertSame($expected, $priorities);
122+
$styles = $worksheet->getConditionalStyles('B1:B5');
123+
self::assertSame(Conditional::CONDITION_CELLIS, $styles[0]->getConditionType());
124+
self::assertSame(40, $styles[0]->getPriority());
125+
self::assertSame(Conditional::CONDITION_EXPRESSION, $styles[1]->getConditionType());
126+
self::assertSame(41, $styles[1]->getPriority());
127+
$reloadedSpreadsheet->disconnectWorksheets();
128+
}
129+
}

tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ public function testStyles(): void
6666
$file .= '#xl/worksheets/sheet1.xml';
6767
$data = file_get_contents($file) ?: '';
6868
$expected = '<conditionalFormatting sqref="C16:C38 E17:H18 I17:J37 D18 J23:J38 E38 I38">'
69-
. '<cfRule type="containsText" dxfId="0" priority="1" operator="containsText" text="Oui">'
69+
. '<cfRule type="containsText" dxfId="0" priority="15" operator="containsText" text="Oui">'
7070
. '<formula>NOT(ISERROR(SEARCH(&quot;Oui&quot;,C16)))</formula>'
7171
. '</cfRule>'
7272
. '</conditionalFormatting>';
7373
self::assertStringContainsString($expected, $data, 'first condition for D18');
7474
$expected = '<conditionalFormatting sqref="C16:C38 I17:J37 E17:H18 D18 J23:J38 E38 I38">'
75-
. '<cfRule type="containsText" dxfId="1" priority="2" operator="containsText" text="Non">'
75+
. '<cfRule type="containsText" dxfId="1" priority="14" operator="containsText" text="Non">'
7676
. '<formula>NOT(ISERROR(SEARCH(&quot;Non&quot;,C16)))</formula>'
7777
. '</cfRule>'
7878
. '</conditionalFormatting>';
15.7 KB
Binary file not shown.

0 commit comments

Comments
 (0)